pointer events generated by mouse events

This commit is contained in:
tsauerwein
2014-02-03 14:37:59 +01:00
parent 1a40b1793c
commit 7140f608f8
6 changed files with 1065 additions and 208 deletions

View File

@@ -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.<string>} 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.<string, function(Event)>} 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];
};

View File

@@ -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);
};

View File

@@ -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.<string, ?>=} 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.<string, ?>}
*/
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.<string>}
*/
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
];

View File

@@ -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.<string>} 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.<string>} 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.<string>}
*/
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
];