]> dev.renevier.net Git - syp.git/blob - openlayers/lib/OpenLayers/Events.js
initial commit
[syp.git] / openlayers / lib / OpenLayers / Events.js
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. */
4
5
6 /**
7  * @requires OpenLayers/Util.js
8  */
9
10 /**
11  * Namespace: OpenLayers.Event
12  * Utility functions for event handling.
13  */
14 OpenLayers.Event = {
15
16     /** 
17      * Property: observers 
18      * {Object} A hashtable cache of the event observers. Keyed by
19      * element._eventCacheID 
20      */
21     observers: false,
22     
23     /** 
24      * Constant: KEY_BACKSPACE 
25      * {int} 
26      */
27     KEY_BACKSPACE: 8,
28
29     /** 
30      * Constant: KEY_TAB 
31      * {int} 
32      */
33     KEY_TAB: 9,
34
35     /** 
36      * Constant: KEY_RETURN 
37      * {int} 
38      */
39     KEY_RETURN: 13,
40
41     /** 
42      * Constant: KEY_ESC 
43      * {int} 
44      */
45     KEY_ESC: 27,
46
47     /** 
48      * Constant: KEY_LEFT 
49      * {int} 
50      */
51     KEY_LEFT: 37,
52
53     /** 
54      * Constant: KEY_UP 
55      * {int} 
56      */
57     KEY_UP: 38,
58
59     /** 
60      * Constant: KEY_RIGHT 
61      * {int} 
62      */
63     KEY_RIGHT: 39,
64
65     /** 
66      * Constant: KEY_DOWN 
67      * {int} 
68      */
69     KEY_DOWN: 40,
70
71     /** 
72      * Constant: KEY_DELETE 
73      * {int} 
74      */
75     KEY_DELETE: 46,
76
77
78     /**
79      * Method: element
80      * Cross browser event element detection.
81      * 
82      * Parameters:
83      * event - {Event} 
84      * 
85      * Returns:
86      * {DOMElement} The element that caused the event 
87      */
88     element: function(event) {
89         return event.target || event.srcElement;
90     },
91
92     /**
93      * Method: isLeftClick
94      * Determine whether event was caused by a left click. 
95      *
96      * Parameters:
97      * event - {Event} 
98      * 
99      * Returns:
100      * {Boolean}
101      */
102     isLeftClick: function(event) {
103         return (((event.which) && (event.which == 1)) ||
104                 ((event.button) && (event.button == 1)));
105     },
106
107     /**
108      * Method: isRightClick
109      * Determine whether event was caused by a right mouse click. 
110      *
111      * Parameters:
112      * event - {Event} 
113      * 
114      * Returns:
115      * {Boolean}
116      */
117      isRightClick: function(event) {
118         return (((event.which) && (event.which == 3)) ||
119                 ((event.button) && (event.button == 2)));
120     },
121      
122     /**
123      * Method: stop
124      * Stops an event from propagating. 
125      *
126      * Parameters: 
127      * event - {Event} 
128      * allowDefault - {Boolean} If true, we stop the event chain but 
129      *                               still allow the default browser 
130      *                               behaviour (text selection, radio-button 
131      *                               clicking, etc)
132      *                               Default false
133      */
134     stop: function(event, allowDefault) {
135         
136         if (!allowDefault) { 
137             if (event.preventDefault) {
138                 event.preventDefault();
139             } else {
140                 event.returnValue = false;
141             }
142         }
143                 
144         if (event.stopPropagation) {
145             event.stopPropagation();
146         } else {
147             event.cancelBubble = true;
148         }
149     },
150
151     /** 
152      * Method: findElement
153      * 
154      * Parameters:
155      * event - {Event} 
156      * tagName - {String} 
157      * 
158      * Returns:
159      * {DOMElement} The first node with the given tagName, starting from the
160      * node the event was triggered on and traversing the DOM upwards
161      */
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;
167         }
168         return element;
169     },
170
171     /** 
172      * Method: observe
173      * 
174      * Parameters:
175      * elementParam - {DOMElement || String} 
176      * name - {String} 
177      * observer - {function} 
178      * useCapture - {Boolean} 
179      */
180     observe: function(elementParam, name, observer, useCapture) {
181         var element = OpenLayers.Util.getElement(elementParam);
182         useCapture = useCapture || false;
183
184         if (name == 'keypress' &&
185            (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
186            || element.attachEvent)) {
187             name = 'keydown';
188         }
189
190         //if observers cache has not yet been created, create it
191         if (!this.observers) {
192             this.observers = {};
193         }
194
195         //if not already assigned, make a new unique cache ID
196         if (!element._eventCacheID) {
197             var idPrefix = "eventCacheID_";
198             if (element.id) {
199                 idPrefix = element.id + "_" + idPrefix;
200             }
201             element._eventCacheID = OpenLayers.Util.createUniqueID(idPrefix);
202         }
203
204         var cacheID = element._eventCacheID;
205
206         //if there is not yet a hash entry for this element, add one
207         if (!this.observers[cacheID]) {
208             this.observers[cacheID] = [];
209         }
210
211         //add a new observer to this element's list
212         this.observers[cacheID].push({
213             'element': element,
214             'name': name,
215             'observer': observer,
216             'useCapture': useCapture
217         });
218
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);
224         }
225     },
226
227     /** 
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.
232      * 
233      * parameters:
234      * elementParam - {DOMElement || String} 
235      */
236     stopObservingElement: function(elementParam) {
237         var element = OpenLayers.Util.getElement(elementParam);
238         var cacheID = element._eventCacheID;
239
240         this._removeElementObservers(OpenLayers.Event.observers[cacheID]);
241     },
242
243     /**
244      * Method: _removeElementObservers
245      *
246      * Parameters:
247      * elementObservers - {Array(Object)} Array of (element, name, 
248      *                                         observer, usecapture) objects, 
249      *                                         taken directly from hashtable
250      */
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,
256                                      entry.name,
257                                      entry.observer,
258                                      entry.useCapture);
259                 var removed = OpenLayers.Event.stopObserving.apply(this, args);
260             }
261         }
262     },
263
264     /**
265      * Method: stopObserving
266      * 
267      * Parameters:
268      * elementParam - {DOMElement || String} 
269      * name - {String} 
270      * observer - {function} 
271      * useCapture - {Boolean} 
272      *  
273      * Returns:
274      * {Boolean} Whether or not the event observer was removed
275      */
276     stopObserving: function(elementParam, name, observer, useCapture) {
277         useCapture = useCapture || false;
278     
279         var element = OpenLayers.Util.getElement(elementParam);
280         var cacheID = element._eventCacheID;
281
282         if (name == 'keypress') {
283             if ( navigator.appVersion.match(/Konqueror|Safari|KHTML/) || 
284                  element.detachEvent) {
285               name = 'keydown';
286             }
287         }
288
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) {
293     
294             // find the specific event type in the element's list
295             var i=0;
296             while(!foundEntry && i < elementObservers.length) {
297                 var cacheEntry = elementObservers[i];
298     
299                 if ((cacheEntry.name == name) &&
300                     (cacheEntry.observer == observer) &&
301                     (cacheEntry.useCapture == useCapture)) {
302     
303                     elementObservers.splice(i, 1);
304                     if (elementObservers.length == 0) {
305                         delete OpenLayers.Event.observers[cacheID];
306                     }
307                     foundEntry = true;
308                     break; 
309                 }
310                 i++;           
311             }
312         }
313     
314         //actually remove the event listener from browser
315         if (foundEntry) {
316             if (element.removeEventListener) {
317                 element.removeEventListener(name, observer, useCapture);
318             } else if (element && element.detachEvent) {
319                 element.detachEvent('on' + name, observer);
320             }
321         }
322         return foundEntry;
323     },
324     
325     /** 
326      * Method: unloadCache
327      * Cycle through all the element entries in the events cache and call
328      *   stopObservingElement on each. 
329      */
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
333         // created
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, 
338                                                            [elementObservers]);
339             }
340             OpenLayers.Event.observers = false;
341         }
342     },
343
344     CLASS_NAME: "OpenLayers.Event"
345 };
346
347 /* prevent memory leaks in IE */
348 OpenLayers.Event.observe(window, 'unload', OpenLayers.Event.unloadCache, false);
349
350 // FIXME: Remove this in 3.0. In 3.0, Event.stop will no longer be provided
351 // by OpenLayers.
352 if (window.Event) {
353     OpenLayers.Util.applyDefaults(window.Event, OpenLayers.Event);
354 } else {
355     var Event = OpenLayers.Event;
356 }
357
358 /**
359  * Class: OpenLayers.Events
360  */
361 OpenLayers.Events = OpenLayers.Class({
362
363     /** 
364      * Constant: BROWSER_EVENTS
365      * {Array(String)} supported events 
366      */
367     BROWSER_EVENTS: [
368         "mouseover", "mouseout",
369         "mousedown", "mouseup", "mousemove", 
370         "click", "dblclick", "rightclick", "dblrightclick",
371         "resize", "focus", "blur"
372     ],
373
374     /** 
375      * Property: listeners 
376      * {Object} Hashtable of Array(Function): events listener functions  
377      */
378     listeners: null,
379
380     /** 
381      * Property: object 
382      * {Object}  the code object issuing application events 
383      */
384     object: null,
385
386     /** 
387      * Property: element 
388      * {DOMElement}  the DOM element receiving browser events 
389      */
390     element: null,
391
392     /** 
393      * Property: eventTypes 
394      * {Array(String)}  list of support application events 
395      */
396     eventTypes: null,
397
398     /** 
399      * Property: eventHandler 
400      * {Function}  bound event handler attached to elements 
401      */
402     eventHandler: null,
403
404     /** 
405      * APIProperty: fallThrough 
406      * {Boolean} 
407      */
408     fallThrough: null,
409
410     /** 
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) 
421      *    } 
422      *
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.
427      *
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
435     */
436     includeXY: false,      
437
438     /**
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>.
443      */
444     clearMouseListener: null,
445
446     /**
447      * Constructor: OpenLayers.Events
448      * Construct an OpenLayers.Events object.
449      *
450      * Parameters:
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
455      *                         been handled?
456      * options - {Object} Options for the events object.
457      */
458     initialize: function (object, element, eventTypes, fallThrough, options) {
459         OpenLayers.Util.extend(this, options);
460         this.object     = object;
461         this.fallThrough = fallThrough;
462         this.listeners  = {};
463
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
468         );
469         
470         // to be used with observe and stopObserving
471         this.clearMouseListener = OpenLayers.Function.bind(
472             this.clearMouseCache, this
473         );
474
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]);
481             }
482         }
483         
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);
488         }
489     },
490
491     /**
492      * APIMethod: destroy
493      */
494     destroy: function () {
495         if (this.element) {
496             OpenLayers.Event.stopObservingElement(this.element);
497             if(this.element.hasScrollEvent) {
498                 OpenLayers.Event.stopObserving(
499                     window, "scroll", this.clearMouseListener
500                 );
501             }
502         }
503         this.element = null;
504
505         this.listeners = null;
506         this.object = null;
507         this.eventTypes = null;
508         this.fallThrough = null;
509         this.eventHandler = null;
510     },
511
512     /**
513      * APIMethod: addEventType
514      * Add a new event type to this events object.
515      * If the event type has already been added, do nothing.
516      * 
517      * Parameters:
518      * eventName - {String}
519      */
520     addEventType: function(eventName) {
521         if (!this.listeners[eventName]) {
522             this.eventTypes.push(eventName);
523             this.listeners[eventName] = [];
524         }
525     },
526
527     /**
528      * Method: attachToElement
529      *
530      * Parameters:
531      * element - {HTMLDOMElement} a DOM element to attach browser events to
532      */
533     attachToElement: function (element) {
534         if(this.element) {
535             OpenLayers.Event.stopObservingElement(this.element);
536         }
537         this.element = element;
538         for (var i=0, len=this.BROWSER_EVENTS.length; i<len; i++) {
539             var eventType = this.BROWSER_EVENTS[i];
540
541             // every browser event has a corresponding application event 
542             // (whether it's listened for or not).
543             this.addEventType(eventType);
544             
545             // use Prototype to register the event cross-browser
546             OpenLayers.Event.observe(element, eventType, this.eventHandler);
547         }
548         // disable dragstart in IE so that mousedown/move/up works normally
549         OpenLayers.Event.observe(element, "dragstart", OpenLayers.Event.stop);
550     },
551     
552     /**
553      * Method: on
554      * Convenience method for registering listeners with a common scope.
555      *
556      * Example use:
557      * (code)
558      * events.on({
559      *     "loadstart": loadStartListener,
560      *     "loadend": loadEndListener,
561      *     scope: object
562      * });
563      * (end)
564      */
565     on: function(object) {
566         for(var type in object) {
567             if(type != "scope") {
568                 this.register(type, object.scope, object[type]);
569             }
570         }
571     },
572
573     /**
574      * APIMethod: register
575      * Register an event on the events object.
576      *
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;
585      *   
586      *                   or
587      *  
588      * :    centerStr = "Center: " + this.getCenterLonLat();
589      *
590      * Parameters:
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 
594      *                     'object' property.
595      * func - {Function} The callback function. If no callback is 
596      *                        specified, this function does nothing.
597      * 
598      * 
599      */
600     register: function (type, obj, func) {
601
602         if ( (func != null) && 
603              (OpenLayers.Util.indexOf(this.eventTypes, type) != -1) ) {
604
605             if (obj == null)  {
606                 obj = this.object;
607             }
608             var listeners = this.listeners[type];
609             listeners.push( {obj: obj, func: func} );
610         }
611     },
612
613     /**
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.
617      *    
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.
620      *
621      *
622      * Parameters:
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 
626      *                'object' property.
627      * func - {Function} The callback function. If no callback is 
628      *                   specified, this function does nothing.
629      */
630     registerPriority: function (type, obj, func) {
631
632         if (func != null) {
633             if (obj == null)  {
634                 obj = this.object;
635             }
636             var listeners = this.listeners[type];
637             if (listeners != null) {
638                 listeners.unshift( {obj: obj, func: func} );
639             }
640         }
641     },
642     
643     /**
644      * Method: un
645      * Convenience method for unregistering listeners with a common scope.
646      *
647      * Example use:
648      * (code)
649      * events.un({
650      *     "loadstart": loadStartListener,
651      *     "loadend": loadEndListener,
652      *     scope: object
653      * });
654      * (end)
655      */
656     un: function(object) {
657         for(var type in object) {
658             if(type != "scope") {
659                 this.unregister(type, object.scope, object[type]);
660             }
661         }
662     },
663
664     /**
665      * APIMethod: unregister
666      *
667      * Parameters:
668      * type - {String} 
669      * obj - {Object} If none specified, defaults to this.object
670      * func - {Function} 
671      */
672     unregister: function (type, obj, func) {
673         if (obj == null)  {
674             obj = this.object;
675         }
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);
681                     break;
682                 }
683             }
684         }
685     },
686
687     /** 
688      * Method: remove
689      * Remove all listeners for a given event type. If type is not registered,
690      *     does nothing.
691      *
692      * Parameters:
693      * type - {String} 
694      */
695     remove: function(type) {
696         if (this.listeners[type] != null) {
697             this.listeners[type] = [];
698         }
699     },
700
701     /**
702      * APIMethod: triggerEvent
703      * Trigger a specified registered event.  
704      * 
705      * Parameters:
706      * type - {String} 
707      * evt - {Event}
708      *
709      * Returns:
710      * {Boolean} The last listener return.  If a listener returns false, the
711      *     chain of listeners will stop getting called.
712      */
713     triggerEvent: function (type, evt) {
714         var listeners = this.listeners[type];
715
716         // fast path
717         if(!listeners || listeners.length == 0) {
718             return;
719         }
720
721         // prep evt object with object & div references
722         if (evt == null) {
723             evt = {};
724         }
725         evt.object = this.object;
726         evt.element = this.element;
727         if(!evt.type) {
728             evt.type = type;
729         }
730     
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]);
739
740             if ((continueChain != undefined) && (continueChain == false)) {
741                 // if callback returns false, execute no more callbacks.
742                 break;
743             }
744         }
745         // don't fall through to other DOM elements
746         if (!this.fallThrough) {           
747             OpenLayers.Event.stop(evt, true);
748         }
749         return continueChain;
750     },
751
752     /**
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 
756      *     position.
757      *
758      * Parameters:
759      * evt - {Event} 
760      */
761     handleBrowserEvent: function (evt) {
762         if (this.includeXY) {
763             evt.xy = this.getMousePosition(evt);
764         } 
765         this.triggerEvent(evt.type, evt);
766     },
767
768     /**
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 
772      *     within the page.
773      */
774     clearMouseCache: function() { 
775         this.element.scrolls = null;
776         this.element.lefttop = null;
777         this.element.offsets = null;
778     },      
779
780     /**
781      * Method: getMousePosition
782      * 
783      * Parameters:
784      * evt - {Event} 
785      * 
786      * Returns:
787      * {<OpenLayers.Pixel>} The current xy coordinate of the mouse, adjusted
788      *                      for offsets
789      */
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;
796         }
797         
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)
804             ];
805         }
806
807         if (!this.element.lefttop) {
808             this.element.lefttop = [
809                 (document.documentElement.clientLeft || 0),
810                 (document.documentElement.clientTop  || 0)
811             ];
812         }
813         
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];
818         }
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]
824         ); 
825     },
826
827     CLASS_NAME: "OpenLayers.Events"
828 });