/* Copyright (c) 2006-2013 by OpenLayers Contributors (see authors.txt for * full list of contributors). Published under the 2-clause BSD license. * See license.txt in the OpenLayers distribution or repository for the * full text of the license. */ /** * @requires OpenLayers/Util.js */ /** * Namespace: OpenLayers.Event * Utility functions for event handling. */ OpenLayers.Event = { /** * Property: observers * {Object} A hashtable cache of the event observers. Keyed by * element._eventCacheID */ observers: false, /** * Constant: KEY_SPACE * {int} */ KEY_SPACE: 32, /** * Constant: KEY_BACKSPACE * {int} */ KEY_BACKSPACE: 8, /** * Constant: KEY_TAB * {int} */ KEY_TAB: 9, /** * Constant: KEY_RETURN * {int} */ KEY_RETURN: 13, /** * Constant: KEY_ESC * {int} */ KEY_ESC: 27, /** * Constant: KEY_LEFT * {int} */ KEY_LEFT: 37, /** * Constant: KEY_UP * {int} */ KEY_UP: 38, /** * Constant: KEY_RIGHT * {int} */ KEY_RIGHT: 39, /** * Constant: KEY_DOWN * {int} */ KEY_DOWN: 40, /** * Constant: KEY_DELETE * {int} */ KEY_DELETE: 46, /** * Method: element * Cross browser event element detection. * * Parameters: * event - {Event} * * Returns: * {DOMElement} The element that caused the event */ element: function(event) { return event.target || event.srcElement; }, /** * Method: isSingleTouch * Determine whether event was caused by a single touch * * Parameters: * event - {Event} * * Returns: * {Boolean} */ isSingleTouch: function(event) { return event.touches && event.touches.length == 1; }, /** * Method: isMultiTouch * Determine whether event was caused by a multi touch * * Parameters: * event - {Event} * * Returns: * {Boolean} */ isMultiTouch: function(event) { return event.touches && event.touches.length > 1; }, /** * Method: isLeftClick * Determine whether event was caused by a left click. * * Parameters: * event - {Event} * * Returns: * {Boolean} */ isLeftClick: function(event) { return (((event.which) && (event.which == 1)) || ((event.button) && (event.button == 1))); }, /** * Method: isRightClick * Determine whether event was caused by a right mouse click. * * Parameters: * event - {Event} * * Returns: * {Boolean} */ isRightClick: function(event) { return (((event.which) && (event.which == 3)) || ((event.button) && (event.button == 2))); }, /** * Method: stop * Stops an event from propagating. * * Parameters: * event - {Event} * allowDefault - {Boolean} If true, we stop the event chain but * still allow the default browser behaviour (text selection, * radio-button clicking, etc). Default is false. */ stop: function(event, allowDefault) { if (!allowDefault) { OpenLayers.Event.preventDefault(event); } if (event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } }, /** * Method: preventDefault * Cancels the event if it is cancelable, without stopping further * propagation of the event. * * Parameters: * event - {Event} */ preventDefault: function(event) { if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }, /** * Method: findElement * * Parameters: * event - {Event} * tagName - {String} * * Returns: * {DOMElement} The first node with the given tagName, starting from the * node the event was triggered on and traversing the DOM upwards */ findElement: function(event, tagName) { var element = OpenLayers.Event.element(event); while (element.parentNode && (!element.tagName || (element.tagName.toUpperCase() != tagName.toUpperCase()))){ element = element.parentNode; } return element; }, /** * Method: observe * * Parameters: * elementParam - {DOMElement || String} * name - {String} * observer - {function} * useCapture - {Boolean} */ observe: function(elementParam, name, observer, useCapture) { var element = OpenLayers.Util.getElement(elementParam); useCapture = useCapture || false; if (name == 'keypress' && (navigator.appVersion.match(/Konqueror|Safari|KHTML/) || element.attachEvent)) { name = 'keydown'; } //if observers cache has not yet been created, create it if (!this.observers) { this.observers = {}; } //if not already assigned, make a new unique cache ID if (!element._eventCacheID) { var idPrefix = "eventCacheID_"; if (element.id) { idPrefix = element.id + "_" + idPrefix; } element._eventCacheID = OpenLayers.Util.createUniqueID(idPrefix); } var cacheID = element._eventCacheID; //if there is not yet a hash entry for this element, add one if (!this.observers[cacheID]) { this.observers[cacheID] = []; } //add a new observer to this element's list this.observers[cacheID].push({ 'element': element, 'name': name, 'observer': observer, 'useCapture': useCapture }); //add the actual browser event listener if (element.addEventListener) { element.addEventListener(name, observer, useCapture); } else if (element.attachEvent) { element.attachEvent('on' + name, observer); } }, /** * Method: stopObservingElement * Given the id of an element to stop observing, cycle through the * element's cached observers, calling stopObserving on each one, * skipping those entries which can no longer be removed. * * parameters: * elementParam - {DOMElement || String} */ stopObservingElement: function(elementParam) { var element = OpenLayers.Util.getElement(elementParam); var cacheID = element._eventCacheID; this._removeElementObservers(OpenLayers.Event.observers[cacheID]); }, /** * Method: _removeElementObservers * * Parameters: * elementObservers - {Array(Object)} Array of (element, name, * observer, usecapture) objects, * taken directly from hashtable */ _removeElementObservers: function(elementObservers) { if (elementObservers) { for(var i = elementObservers.length-1; i >= 0; i--) { var entry = elementObservers[i]; OpenLayers.Event.stopObserving.apply(this, [ entry.element, entry.name, entry.observer, entry.useCapture ]); } } }, /** * Method: stopObserving * * Parameters: * elementParam - {DOMElement || String} * name - {String} * observer - {function} * useCapture - {Boolean} * * Returns: * {Boolean} Whether or not the event observer was removed */ stopObserving: function(elementParam, name, observer, useCapture) { useCapture = useCapture || false; var element = OpenLayers.Util.getElement(elementParam); var cacheID = element._eventCacheID; if (name == 'keypress') { if ( navigator.appVersion.match(/Konqueror|Safari|KHTML/) || element.detachEvent) { name = 'keydown'; } } // find element's entry in this.observers cache and remove it var foundEntry = false; var elementObservers = OpenLayers.Event.observers[cacheID]; if (elementObservers) { // find the specific event type in the element's list var i=0; while(!foundEntry && i < elementObservers.length) { var cacheEntry = elementObservers[i]; if ((cacheEntry.name == name) && (cacheEntry.observer == observer) && (cacheEntry.useCapture == useCapture)) { elementObservers.splice(i, 1); if (elementObservers.length == 0) { delete OpenLayers.Event.observers[cacheID]; } foundEntry = true; break; } i++; } } //actually remove the event listener from browser if (foundEntry) { if (element.removeEventListener) { element.removeEventListener(name, observer, useCapture); } else if (element && element.detachEvent) { element.detachEvent('on' + name, observer); } } return foundEntry; }, /** * Method: unloadCache * Cycle through all the element entries in the events cache and call * stopObservingElement on each. */ unloadCache: function() { // check for OpenLayers.Event before checking for observers, because // OpenLayers.Event may be undefined in IE if no map instance was // created if (OpenLayers.Event && OpenLayers.Event.observers) { for (var cacheID in OpenLayers.Event.observers) { var elementObservers = OpenLayers.Event.observers[cacheID]; OpenLayers.Event._removeElementObservers.apply(this, [elementObservers]); } OpenLayers.Event.observers = false; } }, CLASS_NAME: "OpenLayers.Event" }; /* prevent memory leaks in IE */ OpenLayers.Event.observe(window, 'unload', OpenLayers.Event.unloadCache, false); /** * Class: OpenLayers.Events */ OpenLayers.Events = OpenLayers.Class({ /** * Constant: BROWSER_EVENTS * {Array(String)} supported events */ BROWSER_EVENTS: [ "mouseover", "mouseout", "mousedown", "mouseup", "mousemove", "click", "dblclick", "rightclick", "dblrightclick", "resize", "focus", "blur", "touchstart", "touchmove", "touchend", "keydown" ], /** * Property: listeners * {Object} Hashtable of Array(Function): events listener functions */ listeners: null, /** * Property: object * {Object} the code object issuing application events */ object: null, /** * Property: element * {DOMElement} the DOM element receiving browser events */ element: null, /** * Property: eventHandler * {Function} bound event handler attached to elements */ eventHandler: null, /** * APIProperty: fallThrough * {Boolean} */ fallThrough: null, /** * APIProperty: includeXY * {Boolean} Should the .xy property automatically be created for browser * mouse events? In general, this should be false. If it is true, then * mouse events will automatically generate a '.xy' property on the * event object that is passed. (Prior to OpenLayers 2.7, this was true * by default.) Otherwise, you can call the getMousePosition on the * relevant events handler on the object available via the 'evt.object' * property of the evt object. So, for most events, you can call: * function named(evt) { * this.xy = this.object.events.getMousePosition(evt) * } * * This option typically defaults to false for performance reasons: * when creating an events object whose primary purpose is to manage * relatively positioned mouse events within a div, it may make * sense to set it to true. * * This option is also used to control whether the events object caches * offsets. If this is false, it will not: the reason for this is that * it is only expected to be called many times if the includeXY property * is set to true. If you set this to true, you are expected to clear * the offset cache manually (using this.clearMouseCache()) if: * the border of the element changes * the location of the element in the page changes */ includeXY: false, /** * APIProperty: extensions * {Object} Event extensions registered with this instance. Keys are * event types, values are {OpenLayers.Events.*} extension instances or * {Boolean} for events that an instantiated extension provides in * addition to the one it was created for. * * Extensions create an event in addition to browser events, which usually * fires when a sequence of browser events is completed. Extensions are * automatically instantiated when a listener is registered for an event * provided by an extension. * * Extensions are created in the namespace using * , and named after the event they provide. * The constructor receives the target instance as * argument. Extensions that need to capture browser events before they * propagate can register their listeners events using , with * {extension: true} as 4th argument. * * If an extension creates more than one event, an alias for each event * type should be created and reference the same class. The constructor * should set a reference in the target's extensions registry to itself. * * Below is a minimal extension that provides the "foostart" and "fooend" * event types, which replace the native "click" event type if clicked on * an element with the css class "foo": * * (code) * OpenLayers.Events.foostart = OpenLayers.Class({ * initialize: function(target) { * this.target = target; * this.target.register("click", this, this.doStuff, {extension: true}); * // only required if extension provides more than one event type * this.target.extensions["foostart"] = true; * this.target.extensions["fooend"] = true; * }, * destroy: function() { * var target = this.target; * target.unregister("click", this, this.doStuff); * delete this.target; * // only required if extension provides more than one event type * delete target.extensions["foostart"]; * delete target.extensions["fooend"]; * }, * doStuff: function(evt) { * var propagate = true; * if (OpenLayers.Event.element(evt).className === "foo") { * propagate = false; * var target = this.target; * target.triggerEvent("foostart"); * window.setTimeout(function() { * target.triggerEvent("fooend"); * }, 1000); * } * return propagate; * } * }); * // only required if extension provides more than one event type * OpenLayers.Events.fooend = OpenLayers.Events.foostart; * (end) * */ extensions: null, /** * Property: extensionCount * {Object} Keys are event types (like in ), values are the * number of extension listeners for each event type. */ extensionCount: null, /** * Method: clearMouseListener * A version of that is bound to this instance so that * it can be used with and * . */ clearMouseListener: null, /** * Constructor: OpenLayers.Events * Construct an OpenLayers.Events object. * * Parameters: * object - {Object} The js object to which this Events object is being added * element - {DOMElement} A dom element to respond to browser events * eventTypes - {Array(String)} Deprecated. Array of custom application * events. A listener may be registered for any named event, regardless * of the values provided here. * fallThrough - {Boolean} Allow events to fall through after these have * been handled? * options - {Object} Options for the events object. */ initialize: function (object, element, eventTypes, fallThrough, options) { OpenLayers.Util.extend(this, options); this.object = object; this.fallThrough = fallThrough; this.listeners = {}; this.extensions = {}; this.extensionCount = {}; this._msTouches = []; // if a dom element is specified, add a listeners list // for browser events on the element and register them if (element != null) { this.attachToElement(element); } }, /** * APIMethod: destroy */ destroy: function () { for (var e in this.extensions) { if (typeof this.extensions[e] !== "boolean") { this.extensions[e].destroy(); } } this.extensions = null; if (this.element) { OpenLayers.Event.stopObservingElement(this.element); if(this.element.hasScrollEvent) { OpenLayers.Event.stopObserving( window, "scroll", this.clearMouseListener ); } } this.element = null; this.listeners = null; this.object = null; this.fallThrough = null; this.eventHandler = null; }, /** * APIMethod: addEventType * Deprecated. Any event can be triggered without adding it first. * * Parameters: * eventName - {String} */ addEventType: function(eventName) { }, /** * Method: attachToElement * * Parameters: * element - {HTMLDOMElement} a DOM element to attach browser events to */ attachToElement: function (element) { if (this.element) { OpenLayers.Event.stopObservingElement(this.element); } else { // keep a bound copy of handleBrowserEvent() so that we can // pass the same function to both Event.observe() and .stopObserving() this.eventHandler = OpenLayers.Function.bindAsEventListener( this.handleBrowserEvent, this ); // to be used with observe and stopObserving this.clearMouseListener = OpenLayers.Function.bind( this.clearMouseCache, this ); } this.element = element; var msTouch = !!window.navigator.msMaxTouchPoints; var type; for (var i = 0, len = this.BROWSER_EVENTS.length; i < len; i++) { type = this.BROWSER_EVENTS[i]; // register the event cross-browser OpenLayers.Event.observe(element, type, this.eventHandler ); if (msTouch && type.indexOf('touch') === 0) { this.addMsTouchListener(element, type, this.eventHandler); } } // disable dragstart in IE so that mousedown/move/up works normally OpenLayers.Event.observe(element, "dragstart", OpenLayers.Event.stop); }, /** * APIMethod: on * Convenience method for registering listeners with a common scope. * Internally, this method calls as shown in the examples * below. * * Example use: * (code) * // register a single listener for the "loadstart" event * events.on({"loadstart": loadStartListener}); * * // this is equivalent to the following * events.register("loadstart", undefined, loadStartListener); * * // register multiple listeners to be called with the same `this` object * events.on({ * "loadstart": loadStartListener, * "loadend": loadEndListener, * scope: object * }); * * // this is equivalent to the following * events.register("loadstart", object, loadStartListener); * events.register("loadend", object, loadEndListener); * (end) * * Parameters: * object - {Object} */ on: function(object) { for(var type in object) { if(type != "scope" && object.hasOwnProperty(type)) { this.register(type, object.scope, object[type]); } } }, /** * APIMethod: register * Register an event on the events object. * * When the event is triggered, the 'func' function will be called, in the * context of 'obj'. Imagine we were to register an event, specifying an * OpenLayers.Bounds Object as 'obj'. When the event is triggered, the * context in the callback function will be our Bounds object. This means * that within our callback function, we can access the properties and * methods of the Bounds object through the "this" variable. So our * callback could execute something like: * : leftStr = "Left: " + this.left; * * or * * : centerStr = "Center: " + this.getCenterLonLat(); * * Parameters: * type - {String} Name of the event to register * obj - {Object} The object to bind the context to for the callback#. * If no object is specified, default is the Events's 'object' property. * func - {Function} The callback function. If no callback is * specified, this function does nothing. * priority - {Boolean|Object} If true, adds the new listener to the * *front* of the events queue instead of to the end. * * Valid options for priority: * extension - {Boolean} If true, then the event will be registered as * extension event. Extension events are handled before all other * events. */ register: function (type, obj, func, priority) { if (type in OpenLayers.Events && !this.extensions[type]) { this.extensions[type] = new OpenLayers.Events[type](this); } if (func != null) { if (obj == null) { obj = this.object; } var listeners = this.listeners[type]; if (!listeners) { listeners = []; this.listeners[type] = listeners; this.extensionCount[type] = 0; } var listener = {obj: obj, func: func}; if (priority) { listeners.splice(this.extensionCount[type], 0, listener); if (typeof priority === "object" && priority.extension) { this.extensionCount[type]++; } } else { listeners.push(listener); } } }, /** * APIMethod: registerPriority * Same as register() but adds the new listener to the *front* of the * events queue instead of to the end. * * TODO: get rid of this in 3.0 - Decide whether listeners should be * called in the order they were registered or in reverse order. * * * Parameters: * type - {String} Name of the event to register * obj - {Object} The object to bind the context to for the callback#. * If no object is specified, default is the Events's * 'object' property. * func - {Function} The callback function. If no callback is * specified, this function does nothing. */ registerPriority: function (type, obj, func) { this.register(type, obj, func, true); }, /** * APIMethod: un * Convenience method for unregistering listeners with a common scope. * Internally, this method calls as shown in the examples * below. * * Example use: * (code) * // unregister a single listener for the "loadstart" event * events.un({"loadstart": loadStartListener}); * * // this is equivalent to the following * events.unregister("loadstart", undefined, loadStartListener); * * // unregister multiple listeners with the same `this` object * events.un({ * "loadstart": loadStartListener, * "loadend": loadEndListener, * scope: object * }); * * // this is equivalent to the following * events.unregister("loadstart", object, loadStartListener); * events.unregister("loadend", object, loadEndListener); * (end) */ un: function(object) { for(var type in object) { if(type != "scope" && object.hasOwnProperty(type)) { this.unregister(type, object.scope, object[type]); } } }, /** * APIMethod: unregister * * Parameters: * type - {String} * obj - {Object} If none specified, defaults to this.object * func - {Function} */ unregister: function (type, obj, func) { if (obj == null) { obj = this.object; } var listeners = this.listeners[type]; if (listeners != null) { for (var i=0, len=listeners.length; i Math.floor(evt.pageY) || evt.pageX === 0 && Math.floor(x) > Math.floor(evt.pageX)) { // iOS4 include scroll offset in clientX/Y x = x - winPageX; y = y - winPageY; } else if (y < (evt.pageY - winPageY) || x < (evt.pageX - winPageX) ) { // Some Android browsers have totally bogus values for clientX/Y // when scrolling/zooming a page x = evt.pageX - winPageX; y = evt.pageY - winPageY; } evt.olClientX = x; evt.olClientY = y; return { clientX: x, clientY: y }; }, /** * APIMethod: clearMouseCache * Clear cached data about the mouse position. This should be called any * time the element that events are registered on changes position * within the page. */ clearMouseCache: function() { this.element.scrolls = null; this.element.lefttop = null; this.element.offsets = null; }, /** * Method: getMousePosition * * Parameters: * evt - {Event} * * Returns: * {} The current xy coordinate of the mouse, adjusted * for offsets */ getMousePosition: function (evt) { if (!this.includeXY) { this.clearMouseCache(); } else if (!this.element.hasScrollEvent) { OpenLayers.Event.observe(window, "scroll", this.clearMouseListener); this.element.hasScrollEvent = true; } if (!this.element.scrolls) { var viewportElement = OpenLayers.Util.getViewportElement(); this.element.scrolls = [ window.pageXOffset || viewportElement.scrollLeft, window.pageYOffset || viewportElement.scrollTop ]; } if (!this.element.lefttop) { this.element.lefttop = [ (document.documentElement.clientLeft || 0), (document.documentElement.clientTop || 0) ]; } if (!this.element.offsets) { this.element.offsets = OpenLayers.Util.pagePosition(this.element); } return new OpenLayers.Pixel( (evt.clientX + this.element.scrolls[0]) - this.element.offsets[0] - this.element.lefttop[0], (evt.clientY + this.element.scrolls[1]) - this.element.offsets[1] - this.element.lefttop[1] ); }, /** * Method: addMsTouchListener * * Parameters: * element - {DOMElement} The DOM element to register the listener on * type - {String} The event type * handler - {Function} the handler */ addMsTouchListener: function (element, type, handler) { var eventHandler = this.eventHandler; var touches = this._msTouches; function msHandler(evt) { handler(OpenLayers.Util.applyDefaults({ stopPropagation: function() { for (var i=touches.length-1; i>=0; --i) { touches[i].stopPropagation(); } }, preventDefault: function() { for (var i=touches.length-1; i>=0; --i) { touches[i].preventDefault(); } }, type: type }, evt)); } switch (type) { case 'touchstart': return this.addMsTouchListenerStart(element, type, msHandler); case 'touchend': return this.addMsTouchListenerEnd(element, type, msHandler); case 'touchmove': return this.addMsTouchListenerMove(element, type, msHandler); default: throw 'Unknown touch event type'; } }, /** * Method: addMsTouchListenerStart * * Parameters: * element - {DOMElement} The DOM element to register the listener on * type - {String} The event type * handler - {Function} the handler */ addMsTouchListenerStart: function(element, type, handler) { var touches = this._msTouches; var cb = function(e) { var alreadyInArray = false; for (var i=0, ii=touches.length; i