diff --git a/src/ol/mapbrowserevent.js b/src/ol/mapbrowserevent.js index 24f76bbf38..090faa1b2e 100644 --- a/src/ol/mapbrowserevent.js +++ b/src/ol/mapbrowserevent.js @@ -8,7 +8,8 @@ goog.require('goog.events'); goog.require('goog.events.BrowserEvent'); goog.require('goog.events.EventTarget'); goog.require('goog.events.EventType'); -goog.require('goog.object'); + +goog.require('ol.pointer.PointerEventHandler'); goog.require('ol.Coordinate'); goog.require('ol.MapEvent'); goog.require('ol.Pixel'); @@ -114,12 +115,6 @@ ol.MapBrowserEventHandler = function(map) { */ this.dragListenerKeys_ = null; - /** - * @type {goog.events.Key} - * @private - */ - this.mousedownListenerKey_ = null; - /** * @type {goog.events.Key} * @private @@ -127,10 +122,10 @@ ol.MapBrowserEventHandler = function(map) { this.pointerdownListenerKey_ = null; /** - * @type {goog.events.Key} + * @type {goog.events.BrowserEvent} * @private */ - this.touchstartListenerKey_ = null; + this.down_ = null; if (ol.LEGACY_IE_SUPPORT && ol.IS_LEGACY_IE) { /** @@ -141,38 +136,23 @@ ol.MapBrowserEventHandler = function(map) { } /** - * @type {goog.events.BrowserEvent} + * @type {ol.pointer.PointerEventHandler} * @private */ - this.down_ = null; + this.PointerEventHandler_ = null; var element = this.map_.getViewport(); - this.relayedListenerKeys_ = [ - goog.events.listen(element, - goog.events.EventType.MOUSEMOVE, - this.relayEvent_, false, this), - goog.events.listen(element, - goog.events.EventType.CLICK, - this.relayEvent_, false, this) - ]; - - this.mousedownListenerKey_ = goog.events.listen(element, - goog.events.EventType.MOUSEDOWN, - this.handleMouseDown_, false, this); - - this.pointerdownListenerKey_ = goog.events.listen(element, - goog.events.EventType.MSPOINTERDOWN, + this.pointerEventHandler_ = new ol.pointer.PointerEventHandler(element); + this.pointerdownListenerKey_ = goog.events.listen(this.pointerEventHandler_, + ol.pointer.EventType.POINTERDOWN, this.handlePointerDown_, false, this); - this.touchstartListenerKey_ = goog.events.listen(element, - goog.events.EventType.TOUCHSTART, - this.handleTouchStart_, false, this); - - if (ol.LEGACY_IE_SUPPORT && ol.IS_LEGACY_IE) { - this.ieDblclickListenerKey_ = goog.events.listen(element, - goog.events.EventType.DBLCLICK, this.emulateClick_, false, this); - } + this.relayedListenerKeys_ = [ + goog.events.listen(this.pointerEventHandler_, + ol.pointer.EventType.POINTERMOVE, + this.relayEvent_, false, this) + ]; }; goog.inherits(ol.MapBrowserEventHandler, goog.events.EventTarget); @@ -232,7 +212,7 @@ ol.MapBrowserEventHandler.prototype.emulateClick_ = function(browserEvent) { * @param {goog.events.BrowserEvent} browserEvent Browser event. * @private */ -ol.MapBrowserEventHandler.prototype.handleMouseUp_ = function(browserEvent) { +ol.MapBrowserEventHandler.prototype.handlePointerUp_ = function(browserEvent) { if (this.down_) { goog.array.forEach(this.dragListenerKeys_, goog.events.unlistenByKey); this.dragListenerKeys_ = null; @@ -251,28 +231,17 @@ ol.MapBrowserEventHandler.prototype.handleMouseUp_ = function(browserEvent) { * @param {goog.events.BrowserEvent} browserEvent Browser event. * @private */ -ol.MapBrowserEventHandler.prototype.handleMouseDown_ = function(browserEvent) { - if (!goog.isNull(this.pointerdownListenerKey_)) { - // mouse device detected - unregister the pointerdown and touchstart - // listeners - goog.events.unlistenByKey(this.pointerdownListenerKey_); - this.pointerdownListenerKey_ = null; - - goog.asserts.assert(!goog.isNull(this.touchstartListenerKey_)); - goog.events.unlistenByKey(this.touchstartListenerKey_); - this.touchstartListenerKey_ = null; - } - +ol.MapBrowserEventHandler.prototype.handlePointerDown_ = function(browserEvent) { var newEvent = new ol.MapBrowserEvent( ol.MapBrowserEvent.EventType.DOWN, this.map_, browserEvent); this.dispatchEvent(newEvent); this.down_ = browserEvent; this.dragged_ = false; this.dragListenerKeys_ = [ - goog.events.listen(goog.global.document, goog.events.EventType.MOUSEMOVE, - this.handleMouseMove_, false, this), - goog.events.listen(goog.global.document, goog.events.EventType.MOUSEUP, - this.handleMouseUp_, false, this) + goog.events.listen(this.pointerEventHandler_, ol.MapBrowserEvent.EventType.POINTERMOVE, + this.handlePointerMove_, false, this), + goog.events.listen(this.pointerEventHandler_, ol.MapBrowserEvent.EventType.POINTERUP, + this.handlePointerUp_, false, this) ]; // prevent browser image dragging with the dom renderer browserEvent.preventDefault(); @@ -283,7 +252,7 @@ ol.MapBrowserEventHandler.prototype.handleMouseDown_ = function(browserEvent) { * @param {goog.events.BrowserEvent} browserEvent Browser event. * @private */ -ol.MapBrowserEventHandler.prototype.handleMouseMove_ = function(browserEvent) { +ol.MapBrowserEventHandler.prototype.handlePointerMove_ = function(browserEvent) { var newEvent; if (!this.dragged_) { this.dragged_ = true; @@ -297,159 +266,6 @@ ol.MapBrowserEventHandler.prototype.handleMouseMove_ = function(browserEvent) { }; -/** - * @param {goog.events.BrowserEvent} browserEvent Browser event. - * @private - */ -ol.MapBrowserEventHandler.prototype.handlePointerDown_ = - function(browserEvent) { - if (!goog.isNull(this.mousedownListenerKey_)) { - // pointer device detected - unregister the mousedown and touchstart - // listeners - goog.events.unlistenByKey(this.mousedownListenerKey_); - this.mousedownListenerKey_ = null; - - goog.asserts.assert(!goog.isNull(this.touchstartListenerKey_)); - goog.events.unlistenByKey(this.touchstartListenerKey_); - this.touchstartListenerKey_ = null; - } - - var newEvent = new ol.MapBrowserEvent( - ol.MapBrowserEvent.EventType.TOUCHSTART, this.map_, browserEvent); - this.dispatchEvent(newEvent); - - this.down_ = browserEvent; - this.dragged_ = false; - this.dragListenerKeys_ = [ - goog.events.listen(goog.global.document, - goog.events.EventType.MSPOINTERMOVE, - this.handlePointerMove_, false, this), - goog.events.listen(goog.global.document, goog.events.EventType.MSPOINTERUP, - this.handlePointerUp_, false, this) - ]; - - // FIXME check if/when this is necessary - // prevent context menu - browserEvent.preventDefault(); -}; - - -/** - * @param {goog.events.BrowserEvent} browserEvent Browser event. - * @private - */ -ol.MapBrowserEventHandler.prototype.handlePointerMove_ = - function(browserEvent) { - // Fix IE10 on windows Surface : When you tap the tablet, it triggers - // multiple pointermove events between pointerdown and pointerup with - // the exact same coordinates of the pointerdown event. To avoid a - // 'false' touchmove event to be dispatched , we test if the pointer - // effectively moved. - if (browserEvent.clientX != this.down_.clientX || - browserEvent.clientY != this.down_.clientY) { - this.dragged_ = true; - var newEvent = new ol.MapBrowserEvent( - ol.MapBrowserEvent.EventType.TOUCHMOVE, this.map_, browserEvent); - this.dispatchEvent(newEvent); - } -}; - - -/** - * @param {goog.events.BrowserEvent} browserEvent Browser event. - * @private - */ -ol.MapBrowserEventHandler.prototype.handlePointerUp_ = function(browserEvent) { - var newEvent = new ol.MapBrowserEvent( - ol.MapBrowserEvent.EventType.TOUCHEND, this.map_, browserEvent); - this.dispatchEvent(newEvent); - goog.array.forEach(this.dragListenerKeys_, goog.events.unlistenByKey); - - // We emulate click event on left mouse button click, touch contact, and pen - // contact. isMouseActionButton returns true in these cases (evt.button is set - // to 0). - // See http://www.w3.org/TR/pointerevents/#button-states . - if (!this.dragged_ && browserEvent.isMouseActionButton()) { - goog.asserts.assert(!goog.isNull(this.down_)); - this.emulateClick_(this.down_); - } -}; - - -/** - * @param {goog.events.BrowserEvent} browserEvent Browser event. - * @private - */ -ol.MapBrowserEventHandler.prototype.handleTouchStart_ = function(browserEvent) { - if (!goog.isNull(this.mousedownListenerKey_)) { - // touch device detected - unregister the mousedown and pointerdown - // listeners - goog.events.unlistenByKey(this.mousedownListenerKey_); - this.mousedownListenerKey_ = null; - - goog.asserts.assert(!goog.isNull(this.pointerdownListenerKey_)); - goog.events.unlistenByKey(this.pointerdownListenerKey_); - this.pointerdownListenerKey_ = null; - } - - var newEvent = new ol.MapBrowserEvent( - ol.MapBrowserEvent.EventType.TOUCHSTART, this.map_, browserEvent); - this.dispatchEvent(newEvent); - - this.down_ = browserEvent; - this.dragged_ = false; - - if (goog.isNull(this.dragListenerKeys_)) { - this.dragListenerKeys_ = [ - goog.events.listen(goog.global.document, goog.events.EventType.TOUCHMOVE, - this.handleTouchMove_, false, this), - goog.events.listen(goog.global.document, goog.events.EventType.TOUCHEND, - this.handleTouchEnd_, false, this) - ]; - } - - // FIXME check if/when this is necessary - browserEvent.preventDefault(); -}; - - -/** - * @param {goog.events.BrowserEvent} browserEvent Browser event. - * @private - */ -ol.MapBrowserEventHandler.prototype.handleTouchMove_ = function(browserEvent) { - this.dragged_ = true; - var newEvent = new ol.MapBrowserEvent( - ol.MapBrowserEvent.EventType.TOUCHMOVE, this.map_, browserEvent); - this.dispatchEvent(newEvent); - - // Some native android browser triggers mousemove events during small period - // of time. See: https://code.google.com/p/android/issues/detail?id=5491 or - // https://code.google.com/p/android/issues/detail?id=19827 - // ex: Galaxy Tab P3110 + Android 4.1.1 - browserEvent.preventDefault(); -}; - - -/** - * @param {goog.events.BrowserEvent} browserEvent Browser event. - * @private - */ -ol.MapBrowserEventHandler.prototype.handleTouchEnd_ = function(browserEvent) { - var newEvent = new ol.MapBrowserEvent( - ol.MapBrowserEvent.EventType.TOUCHEND, this.map_, browserEvent); - this.dispatchEvent(newEvent); - if (browserEvent.getBrowserEvent().targetTouches.length === 0) { - goog.array.forEach(this.dragListenerKeys_, goog.events.unlistenByKey); - this.dragListenerKeys_ = null; - } - if (!this.dragged_) { - goog.asserts.assert(!goog.isNull(this.down_)); - this.emulateClick_(this.down_); - } -}; - - /** * FIXME empty description for jsdoc */ @@ -502,13 +318,22 @@ ol.MapBrowserEventHandler.prototype.relayEvent_ = function(browserEvent) { ol.MapBrowserEvent.EventType = { CLICK: goog.events.EventType.CLICK, DBLCLICK: goog.events.EventType.DBLCLICK, - MOUSEMOVE: goog.events.EventType.MOUSEMOVE, - DOWN: 'down', DRAGSTART: 'dragstart', DRAG: 'drag', DRAGEND: 'dragend', + DOWN: 'down', + + MOUSEMOVE: goog.events.EventType.MOUSEMOVE, SINGLECLICK: 'singleclick', TOUCHSTART: goog.events.EventType.TOUCHSTART, TOUCHMOVE: goog.events.EventType.TOUCHMOVE, - TOUCHEND: goog.events.EventType.TOUCHEND + TOUCHEND: goog.events.EventType.TOUCHEND, + + POINTERMOVE: 'pointermove', + POINTERDOWN: 'pointerdown', + POINTERUP: 'pointerup', + POINTEROVER: 'pointerover', + POINTERENTER: 'pointerenter', + POINTERLEAVE: 'pointerleave', + POINTERCANCEL: 'pointercancel' }; diff --git a/src/ol/pointer/eventsource.js b/src/ol/pointer/eventsource.js new file mode 100644 index 0000000000..b966e21054 --- /dev/null +++ b/src/ol/pointer/eventsource.js @@ -0,0 +1,40 @@ + +goog.provide('ol.pointer.EventSource'); + + + +/** + * @param {ol.pointer.PointerEventHandler} dispatcher + * @constructor + */ +ol.pointer.EventSource = function(dispatcher) { + /** + * @type {ol.pointer.PointerEventHandler} + */ + this.dispatcher = dispatcher; +}; + + +/** + * List of events supported by this source. + * @return {Array.} Event names + */ +ol.pointer.EventSource.prototype.getEvents = goog.abstractMethod; + + +/** + * Returns a mapping between the supported event types and + * the handlers that should handle an event. + * @return {Object.} Event/Handler mapping + */ +ol.pointer.EventSource.prototype.getMapping = goog.abstractMethod; + + +/** + * Returns the handler that should handle a given event type. + * @param {string} eventType + * @return {function(Event)} Handler + */ +ol.pointer.EventSource.prototype.getHandlerForEvent = function(eventType) { + return this.getMapping()[eventType]; +}; diff --git a/src/ol/pointer/mousesource.js b/src/ol/pointer/mousesource.js new file mode 100644 index 0000000000..7a1c4446f8 --- /dev/null +++ b/src/ol/pointer/mousesource.js @@ -0,0 +1,197 @@ +goog.provide('ol.pointer.MouseSource'); + +goog.require('ol.pointer.EventSource'); + + + +/** + * @param {ol.pointer.PointerEventHandler} dispatcher + * @constructor + * @extends {ol.pointer.EventSource} + */ +ol.pointer.MouseSource = function(dispatcher) { + goog.base(this, dispatcher); + + this.pointerMap = dispatcher.pointerMap; + + // radius around touchend that swallows mouse events + this.DEDUP_DIST = 25; + + this.POINTER_ID = 1; + this.POINTER_TYPE = 'mouse'; + + this.events = [ + 'mousedown', + 'mousemove', + 'mouseup', + 'mouseover', + 'mouseout' + ]; + this.mapping = { + 'mousedown': this.mousedown, + 'mousemove': this.mousemove, + 'mouseup': this.mouseup, + 'mouseover': this.mouseover, + 'mouseout': this.mouseout + }; + + this.lastTouches = []; +}; +goog.inherits(ol.pointer.MouseSource, ol.pointer.EventSource); + + +/** @inheritDoc */ +ol.pointer.MouseSource.prototype.getEvents = function() { + return this.events; +}; + + +/** @inheritDoc */ +ol.pointer.MouseSource.prototype.getMapping = function() { + return this.mapping; +}; + + +/** + * Collide with the global mouse listener + * + * @private + * @param {goog.events.BrowserEvent} inEvent + * @return {boolean} True, if the event was generated by a touch. + */ +ol.pointer.MouseSource.prototype.isEventSimulatedFromTouch_ = + function(inEvent) { + var lts = this.lastTouches; + var x = inEvent.clientX, y = inEvent.clientY; + for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) { + // simulated mouse events will be swallowed near a primary touchend + var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y); + if (dx <= this.DEDUP_DIST && dy <= this.DEDUP_DIST) { + return true; + } + } + return false; +}; + + +/** + * Creates a copy of the original event that will be used + * for the fake pointer event. + * + * @private + * @param {goog.events.BrowserEvent} inEvent + * @return {Object} + */ +ol.pointer.MouseSource.prototype.prepareEvent_ = function(inEvent) { + var e = this.dispatcher.cloneEvent(inEvent); + + // forward mouse preventDefault + var pd = e.preventDefault; + e.preventDefault = function() { + inEvent.preventDefault(); + pd(); + }; + + e.pointerId = this.POINTER_ID; + e.isPrimary = true; + e.pointerType = this.POINTER_TYPE; + + return e; +}; + + +/** + * Handler for `mousedown`. + * + * @param {goog.events.BrowserEvent} inEvent + */ +ol.pointer.MouseSource.prototype.mousedown = function(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + var p = this.pointerMap.containsKey(this.POINTER_ID); + // TODO(dfreedman) workaround for some elements not sending mouseup + // http://crbug/149091 + if (p) { + this.cancel(inEvent); + } + var e = this.prepareEvent_(inEvent); + this.pointerMap.set(this.POINTER_ID, inEvent); + this.dispatcher.down(e); + } +}; + + +/** + * Handler for `mousemove`. + * + * @param {goog.events.BrowserEvent} inEvent + */ +ol.pointer.MouseSource.prototype.mousemove = function(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + var e = this.prepareEvent_(inEvent); + this.dispatcher.move(e); + } +}; + + +/** + * Handler for `mouseup`. + * + * @param {goog.events.BrowserEvent} inEvent + */ +ol.pointer.MouseSource.prototype.mouseup = function(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + var p = this.pointerMap.get(this.POINTER_ID); + + if (p && p.button === inEvent.button) { + var e = this.prepareEvent_(inEvent); + this.dispatcher.up(e); + this.cleanupMouse(); + } + } +}; + + +/** + * Handler for `mouseover`. + * + * @param {goog.events.BrowserEvent} inEvent + */ +ol.pointer.MouseSource.prototype.mouseover = function(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + var e = this.prepareEvent_(inEvent); + this.dispatcher.enterOver(e); + } +}; + + +/** + * Handler for `mouseout`. + * + * @param {goog.events.BrowserEvent} inEvent + */ +ol.pointer.MouseSource.prototype.mouseout = function(inEvent) { + if (!this.isEventSimulatedFromTouch_(inEvent)) { + var e = this.prepareEvent_(inEvent); + this.dispatcher.leaveOut(e); + } +}; + + +/** + * Dispatches a `pointercancel` event. + * + * @param {goog.events.BrowserEvent} inEvent + */ +ol.pointer.MouseSource.prototype.cancel = function(inEvent) { + var e = this.prepareEvent_(inEvent); + this.dispatcher.cancel(e); + this.cleanupMouse(); +}; + + +/** + * Remove the mouse from the list of active pointers. + */ +ol.pointer.MouseSource.prototype.cleanupMouse = function() { + this.pointerMap.remove(this.POINTER_ID); +}; diff --git a/src/ol/pointer/pointerevent.js b/src/ol/pointer/pointerevent.js new file mode 100644 index 0000000000..e77d8d5686 --- /dev/null +++ b/src/ol/pointer/pointerevent.js @@ -0,0 +1,209 @@ +goog.provide('ol.pointer.PointerEvent'); + + +//goog.require('goog.events.Event'); +goog.require('goog.events'); + + + +/** + * This is the constructor for new PointerEvents. + * + * New Pointer Events must be given a type, and an optional dictionary of + * initialization properties. + * + * Due to certain platform requirements, events returned from the constructor + * identify as MouseEvents. + * + * @constructor + * @extends {Event} + * @param {string} inType The type of the event to create. + * @param {Object.=} opt_inDict An optional dictionary of + * initial event properties. + */ +ol.pointer.PointerEvent = function(inType, opt_inDict) { + opt_inDict = opt_inDict || {}; + // According to the w3c spec, + // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-button + // MouseEvent.button == 0 can mean either no mouse button depressed, or the + // left mouse button depressed. + // + // As of now, the only way to distinguish between the two states of + // MouseEvent.button is by using the deprecated MouseEvent.which property, as + // this maps mouse buttons to positive integers > 0, and uses 0 to mean that + // no mouse button is held. + // + // MouseEvent.which is derived from MouseEvent.button at MouseEvent creation, + // but initMouseEvent does not expose an argument with which to set + // MouseEvent.which. Calling initMouseEvent with a buttonArg of 0 will set + // MouseEvent.button == 0 and MouseEvent.which == 1, breaking the expectations + // of app developers. + // + // The only way to propagate the correct state of MouseEvent.which and + // MouseEvent.button to a new MouseEvent.button == 0 and MouseEvent.which == 0 + // is to call initMouseEvent with a buttonArg value of -1. + // + // This is fixed with DOM Level 4's use of buttons + var buttons; + if (opt_inDict.buttons || ol.pointer.PointerEvent.HAS_BUTTONS) { + buttons = opt_inDict.buttons; + } else { + switch (opt_inDict.which) { + case 1: buttons = 1; break; + case 2: buttons = 4; break; + case 3: buttons = 2; break; + default: buttons = 0; + } + } + + var e; + if (ol.pointer.PointerEvent.NEW_MOUSE_EVENT) { + e = ol.pointer.PointerEvent.createMouseEvent(inType, opt_inDict); + } else { + e = document.createEvent('MouseEvent'); + + // import values from the given dictionary + /** + * @type {Object.} + */ + var props = {}; + var p; + for (var i = 0; i < ol.pointer.PointerEvent.MOUSE_PROPS.length; i++) { + p = ol.pointer.PointerEvent.MOUSE_PROPS[i]; + props[p] = opt_inDict[p] || ol.pointer.PointerEvent.MOUSE_DEFAULTS[i]; + } + + // define the properties inherited from MouseEvent + e.initMouseEvent( + inType, props.bubbles, props.cancelable, props.view, props.detail, + props.screenX, props.screenY, props.clientX, props.clientY, + props.ctrlKey, props.altKey, props.shiftKey, props.metaKey, + props.button, props.relatedTarget + ); + } + + // make the event pass instanceof checks + e.__proto__ = ol.pointer.PointerEvent.prototype; + + // define the buttons property according to DOM Level 3 spec + if (!ol.pointer.PointerEvent.HAS_BUTTONS) { + // IE 10 has buttons on MouseEvent.prototype as a getter w/o any setting + // mechanism + Object.defineProperty(e, 'buttons', + {get: function() { return buttons; }, enumerable: true}); + } + + // Spec requires that pointers without pressure specified use 0.5 for down + // state and 0 for up state. + var pressure = 0; + if (opt_inDict.pressure) { + pressure = opt_inDict.pressure; + } else { + pressure = buttons ? 0.5 : 0; + } + + // define the properties of the PointerEvent interface + Object.defineProperties(e, { + pointerId: { value: opt_inDict.pointerId || 0, enumerable: true }, + width: { value: opt_inDict.width || 0, enumerable: true }, + height: { value: opt_inDict.height || 0, enumerable: true }, + pressure: { value: pressure, enumerable: true }, + tiltX: { value: opt_inDict.tiltX || 0, enumerable: true }, + tiltY: { value: opt_inDict.tiltY || 0, enumerable: true }, + pointerType: { value: opt_inDict.pointerType || '', enumerable: true }, + hwTimestamp: { value: opt_inDict.hwTimestamp || 0, enumerable: true }, + isPrimary: { value: opt_inDict.isPrimary || false, enumerable: true } + }); + + return e; +}; + +// PointerEvent extends MouseEvent +ol.pointer.PointerEvent.prototype = Object.create(MouseEvent.prototype); + + +// test for DOM Level 4 Events + + +/** + * Does the browser support the `MouseEvent` type? + * @type {boolean} + */ +ol.pointer.PointerEvent.NEW_MOUSE_EVENT = false; + + +/** + * Is the `buttons` property supported? + * @type {boolean} + */ +ol.pointer.PointerEvent.HAS_BUTTONS = false; + + +/** + * Checks if the `MouseEvent` type is supported. + */ +ol.pointer.PointerEvent.checkNewMouseEvent = function() { + try { + var ev = ol.pointer.PointerEvent.createMouseEvent('click', {buttons: 1}); + ol.pointer.PointerEvent.NEW_MOUSE_EVENT = true; + ol.pointer.PointerEvent.HAS_BUTTONS = ev.buttons === 1; + } catch (e) { + } +}; +ol.pointer.PointerEvent.checkNewMouseEvent(); + + +/** + * Warning is suppressed because Closure thinks MouseEvent + * has no arguments. + * @param {string} inType The type of the event to create. + * @param {Object} inDict An dictionary of initial event properties. + * @return {MouseEvent} + * @suppress {checkTypes} + */ +ol.pointer.PointerEvent.createMouseEvent = function(inType, inDict) { + return new MouseEvent(inType, inDict); +}; + + +/** + * List of properties to copy when creating an event. + * @type {Array.} + */ +ol.pointer.PointerEvent.MOUSE_PROPS = [ + 'bubbles', + 'cancelable', + 'view', + 'detail', + 'screenX', + 'screenY', + 'clientX', + 'clientY', + 'ctrlKey', + 'altKey', + 'shiftKey', + 'metaKey', + 'button', + 'relatedTarget' +]; + + +/** + * List of default values when creating an event. + */ +ol.pointer.PointerEvent.MOUSE_DEFAULTS = [ + false, + false, + null, + null, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null +]; diff --git a/src/ol/pointer/pointereventhandler.js b/src/ol/pointer/pointereventhandler.js new file mode 100644 index 0000000000..b49455bed4 --- /dev/null +++ b/src/ol/pointer/pointereventhandler.js @@ -0,0 +1,509 @@ +goog.provide('ol.pointer.PointerEventHandler'); + + +goog.require('goog.debug.Console'); +goog.require('goog.events'); +goog.require('goog.events.BrowserEvent'); +goog.require('goog.events.Event'); +goog.require('goog.events.EventTarget'); +goog.require('goog.structs.Map'); + +goog.require('ol.pointer.MouseSource'); +// goog.require('ol.pointer.MsSource'); +// goog.require('ol.pointer.NativeSource'); +goog.require('ol.pointer.PointerEvent'); +// goog.require('ol.pointer.TouchSource'); +goog.require('ol.structs.WeakMap'); + + + +/** + * @constructor + * @extends {goog.events.EventTarget} + * @param {Element} element Viewport element. + */ +ol.pointer.PointerEventHandler = function(element) { + goog.base(this); + + /** + * @const + * @private + * @type {Element} + */ + this.element_ = element; + + /** + * @const + * @type {goog.structs.Map} + */ + this.pointerMap = new goog.structs.Map(); + + /** + * @const + * @type {ol.structs.WeakMap} + */ + this.targets = new ol.structs.WeakMap(); + + /** + * @const + * @type {ol.structs.WeakMap} + */ + this.handledEvents = new ol.structs.WeakMap(); + + this.eventMap = {}; + + // Scope objects for native events. + // This exists for ease of testing. + this.eventSources = {}; + this.eventSourceList = []; + + this.boundHandler_ = this.eventHandler_.bind(this); + + this.registerSources(); +}; +goog.inherits(ol.pointer.PointerEventHandler, goog.events.EventTarget); + + +/** + * Set up the event sources (mouse, touch and native pointers) + * that generate pointer events. + */ +ol.pointer.PointerEventHandler.prototype.registerSources = function() { + if (this.isPointerEnabled_()) { + // this.registerSource('native', new ol.pointer.NativeSource(this)); + } else if (this.isMsPointerEnabled_()) { + // this.registerSource('ms', new ol.pointer.MsSource(this)); + } else { + var mouseSource = new ol.pointer.MouseSource(this); + this.registerSource('mouse', mouseSource); + + if (this.isTouchDefined_()) { + //this.registerSource('touch', + // new ol.pointer.TouchSource(this, mouseSource)); + } + } + + // register events on the viewport element + this.register_(); +}; + + +/** + * @private + * @return {boolean} Returns true if the browser supports + * native pointer events. + */ +ol.pointer.PointerEventHandler.prototype.isPointerEnabled_ = function() { + /* TODO navigation.pointerEnabled is actually not part of the + * spec: https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890#c3 + */ + return window.navigator['pointerEnabled'] !== undefined; +}; + + +/** + * @private + * @return {boolean} Returns true if the browser supports + * ms pointer events (IE10). + */ +ol.pointer.PointerEventHandler.prototype.isMsPointerEnabled_ = function() { + return window.navigator['msPointerEnabled'] !== undefined; +}; + + +/** + * @private + * @return {boolean} Returns true if the browser supports + * touch events. + */ +ol.pointer.PointerEventHandler.prototype.isTouchDefined_ = function() { + return window['ontouchstart'] !== undefined; +}; + + +/** + * Add a new event source that will generate pointer events. + * + * @param {string} name A name for the event source + * @param {ol.pointer.EventSource} source + */ +ol.pointer.PointerEventHandler.prototype.registerSource = + function(name, source) { + var s = source; + var newEvents = s.getEvents(); + + if (newEvents) { + newEvents.forEach(function(e) { + var handler = s.getHandlerForEvent(e); + + if (handler) { + this.eventMap[e] = handler.bind(s); + } + }, this); + this.eventSources[name] = s; + this.eventSourceList.push(s); + } +}; + +/** + * @suppress {undefinedVars} + */ +ol.pointer.PointerEventHandler.prototype.log = function(obj) { + console.log(obj); +} + + +/** + * Set up the events for all registered event sources. + * @private + */ +ol.pointer.PointerEventHandler.prototype.register_ = function() { + var l = this.eventSourceList.length; + for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) { + this.addEvents_(es.getEvents()); + } +}; + + +/** + * Remove all registered events. + * @private + */ +ol.pointer.PointerEventHandler.prototype.unregister_ = function() { + var l = this.eventSourceList.length; + for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) { + this.removeEvents_(es.getEvents()); + } +}; + + +/** + * Calls the right handler for a new event. + * @private + * @param {goog.events.BrowserEvent} inEvent Browser event. + */ +ol.pointer.PointerEventHandler.prototype.eventHandler_ = function(inEvent) { + // This is used to prevent multiple dispatch of pointerevents from + // platform events. This can happen when two elements in different scopes + // are set up to create pointer events, which is relevant to Shadow DOM. + if (this.handledEvents['get'](inEvent)) { + return; + } + + var type = inEvent.type; + var handler = this.eventMap[type]; + if (handler) { + handler(inEvent); + } + this.handledEvents['set'](inEvent, true); +}; + + +/** + * Setup listeners for the given events. + * @private + * @param {Array.} events List of events. + */ +ol.pointer.PointerEventHandler.prototype.addEvents_ = function(events) { + events.forEach(function(eventName) { + goog.events.listen(this.element_, eventName, + this.boundHandler_); + }, this); +}; + + +/** + * Unregister listeners for the given events. + * @private + * @param {Array.} events List of events. + */ +ol.pointer.PointerEventHandler.prototype.removeEvents_ = function(events) { + events.forEach(function(e) { + goog.events.unlisten(this.element_, e, + this.boundHandler_); + }, this); +}; + + +/** + * Returns a snapshot of inEvent, with writable properties. + * + * @param {goog.events.BrowserEvent} inEvent An event that contains + * properties to copy. + * @return {Object} An object containing shallow copies of + * `inEvent`'s properties. + */ +ol.pointer.PointerEventHandler.prototype.cloneEvent = function(inEvent) { + var eventCopy = {}, p; + for (var i = 0; i < ol.pointer.CLONE_PROPS.length; i++) { + p = ol.pointer.CLONE_PROPS[i]; + eventCopy[p] = inEvent[p] || ol.pointer.CLONE_DEFAULTS[i]; + } + + // keep the semantics of preventDefault + if (inEvent.preventDefault) { + eventCopy.preventDefault = function() { + inEvent.preventDefault(); + }; + } + + return eventCopy; +}; + + +// EVENTS + + +/** + * Triggers a 'pointerdown' event. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.down = function(inEvent) { + this.fireEvent('pointerdown', inEvent); +}; + + +/** + * Triggers a 'pointermove' event. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.move = function(inEvent) { + this.fireEvent('pointermove', inEvent); +}; + + +/** + * Triggers a 'pointerup' event. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.up = function(inEvent) { + this.fireEvent('pointerup', inEvent); +}; + + +/** + * Triggers a 'pointerenter' event. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.enter = function(inEvent) { + inEvent.bubbles = false; + this.fireEvent('pointerenter', inEvent); +}; + + +/** + * Triggers a 'pointerleave' event. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.leave = function(inEvent) { + inEvent.bubbles = false; + this.fireEvent('pointerleave', inEvent); +}; + + +/** + * Triggers a 'pointerover' event. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.over = function(inEvent) { + inEvent.bubbles = true; + this.fireEvent('pointerover', inEvent); +}; + + +/** + * Triggers a 'pointerout' event. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.out = function(inEvent) { + inEvent.bubbles = true; + this.fireEvent('pointerout', inEvent); +}; + + +/** + * Triggers a 'pointercancel' event. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.cancel = function(inEvent) { + this.fireEvent('pointercancel', inEvent); +}; + + +/** + * Triggers a combination of 'pointerout' and 'pointerleave' events. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.leaveOut = function(inEvent) { + this.out(inEvent); + if (!this.contains_(inEvent.target, inEvent.relatedTarget)) { + this.leave(inEvent); + } +}; + + +/** + * Triggers a combination of 'pointerover' and 'pointerevents' events. + * @param {Object} inEvent + */ +ol.pointer.PointerEventHandler.prototype.enterOver = function(inEvent) { + this.over(inEvent); + if (!this.contains_(inEvent.target, inEvent.relatedTarget)) { + this.enter(inEvent); + } +}; + + +/** + * @private + * @param {Element} container + * @param {Element} contained + * @return {boolean} Returns true if the container element + * contains the other element. + */ +ol.pointer.PointerEventHandler.prototype.contains_ = + function(container, contained) { + return container.contains(contained); +}; + + +// EVENT CREATION AND TRACKING +/** + * Creates a new Event of type `inType`, based on the information in + * `inEvent`. + * + * @param {string} inType A string representing the type of event to create. + * @param {Object} inEvent A platform event with a target. + * @return {ol.pointer.PointerEvent} A PointerEvent of type `inType`. + */ +ol.pointer.PointerEventHandler.prototype.makeEvent = function(inType, inEvent) { + // relatedTarget must be null if pointer is captured + if (this.captureInfo) { + inEvent.relatedTarget = null; + } + + var e = new ol.pointer.PointerEvent(inType, inEvent); + if (inEvent.preventDefault) { + e.preventDefault = inEvent.preventDefault; + } + this.targets['set'](e, this.targets['get'](inEvent) || inEvent.target); + + return e; +}; + + +/** + * Make and dispatch an event in one call. + * @param {string} inType A string representing the type of event. + * @param {Object} inEvent A platform event with a target. + */ +ol.pointer.PointerEventHandler.prototype.fireEvent = function(inType, inEvent) { + var e = this.makeEvent(inType, inEvent); + var browserEvent = new goog.events.BrowserEvent(e); + this.dispatchEvent(browserEvent); +}; + + +/** + * Re-fires a native pointer event. + * @param {Event} nativeEvent A platform event with a target. + */ +ol.pointer.PointerEventHandler.prototype.fireNativeEvent = + function(nativeEvent) { + var browserEvent = new goog.events.BrowserEvent(nativeEvent); + this.dispatchEvent(browserEvent); +}; + + +/** + * Constants for event names. + * @enum {string} + */ +ol.pointer.EventType = { + POINTERMOVE: 'pointermove', + POINTERDOWN: 'pointerdown', + POINTERUP: 'pointerup', + POINTEROVER: 'pointerover', + POINTERENTER: 'pointerenter', + POINTERLEAVE: 'pointerleave', + POINTERCANCEL: 'pointercancel' +}; + + +/** + * List of properties to copy when cloning an event. + * @type {Array.} + */ +ol.pointer.CLONE_PROPS = [ + // MouseEvent + 'bubbles', + 'cancelable', + 'view', + 'detail', + 'screenX', + 'screenY', + 'clientX', + 'clientY', + 'ctrlKey', + 'altKey', + 'shiftKey', + 'metaKey', + 'button', + 'relatedTarget', + // DOM Level 3 + 'buttons', + // PointerEvent + 'pointerId', + 'width', + 'height', + 'pressure', + 'tiltX', + 'tiltY', + 'pointerType', + 'hwTimestamp', + 'isPrimary', + // event instance + 'type', + 'target', + 'currentTarget', + 'which' +]; + + +/** + * List of default values when cloning an event. + */ +ol.pointer.CLONE_DEFAULTS = [ + // MouseEvent + false, + false, + null, + null, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null, + // DOM Level 3 + 0, + // PointerEvent + 0, + 0, + 0, + 0, + 0, + 0, + '', + 0, + false, + // event instance + '', + null, + null, + 0 +]; diff --git a/src/ol/structs/weakmap.js b/src/ol/structs/weakmap.js new file mode 100644 index 0000000000..0123b602b3 --- /dev/null +++ b/src/ol/structs/weakmap.js @@ -0,0 +1,77 @@ +// Based on https://github.com/Polymer/WeakMap +/* +* Copyright 2012 The Polymer Authors. All rights reserved. +* Use of this source code is governed by a BSD-style +* license that can be found in the LICENSE file. +*/ + + +goog.provide('ol.structs.WeakMap'); + + +/** + * @suppress {undefinedVars} + * @return {boolean} Is `WeakMap` already defined? + */ +ol.structs.isWeakMapUndefined = function() { + return typeof WeakMap === 'undefined'; +}; + + +if (ol.structs.isWeakMapUndefined()) { + /** + * @constructor + */ + ol.structs.WeakMap = function() { + this.name = '__st' + (Math.random() * 1e9 >>> 0) + + (ol.structs.WeakMap.counter++ + '__'); + }; + + + /** + * @param {*} key + * @param {*} value + */ + ol.structs.WeakMap.prototype['set'] = function(key, value) { + var entry = key[this.name]; + if (entry && entry[0] === key) + entry[1] = value; + else + ol.structs.WeakMap.defineProperty(key, this.name, + {value: [key, value], writable: true}); + }; + + + /** + * @param {*} key + * @return {*} + */ + ol.structs.WeakMap.prototype['get'] = function(key) { + var entry; + return (entry = key[this.name]) && entry[0] === key ? + entry[1] : undefined; + }; + + + /** + * @this {ol.structs.WeakMap} + * @param {*} key + */ + ol.structs.WeakMap.prototype['delete'] = function(key) { + this['set'](key, undefined); + }; +} else { + ol.structs.WeakMap = WeakMap; +} + + +/** + * @type {function(...)} + */ +ol.structs.WeakMap.defineProperty = Object.defineProperty; + + +/** + * @type {number} + */ +ol.structs.WeakMap.counter = Date.now() % 1e9;