@qooxdoo/framework
Version:
The JS Framework for Coders
492 lines (422 loc) • 16.5 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2014 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Christopher Zuendorf (czuendorf)
* Daniel Wagner (danielwagner)
************************************************************************ */
/**
* Low-level pointer event handler.
*
* @require(qx.bom.client.Event)
* @require(qx.bom.client.Device)
*/
qx.Bootstrap.define("qx.event.handler.PointerCore", {
extend : Object,
implement: [ qx.core.IDisposable ],
statics : {
MOUSE_TO_POINTER_MAPPING: {
mousedown: "pointerdown",
mouseup: "pointerup",
mousemove: "pointermove",
mouseout: "pointerout",
mouseover: "pointerover"
},
TOUCH_TO_POINTER_MAPPING: {
touchstart: "pointerdown",
touchend: "pointerup",
touchmove: "pointermove",
touchcancel: "pointercancel"
},
MSPOINTER_TO_POINTER_MAPPING: {
MSPointerDown : "pointerdown",
MSPointerMove : "pointermove",
MSPointerUp : "pointerup",
MSPointerCancel : "pointercancel",
MSPointerLeave : "pointerleave",
MSPointerEnter: "pointerenter",
MSPointerOver : "pointerover",
MSPointerOut : "pointerout"
},
POINTER_TO_GESTURE_MAPPING : {
pointerdown : "gesturebegin",
pointerup : "gesturefinish",
pointercancel : "gesturecancel",
pointermove : "gesturemove"
},
LEFT_BUTTON : (qx.core.Environment.get("engine.name") == "mshtml" &&
qx.core.Environment.get("browser.documentmode") <= 8) ? 1 : 0,
SIM_MOUSE_DISTANCE : 25,
SIM_MOUSE_DELAY : 2500,
/**
* Coordinates of the last touch. This needs to be static because the target could
* change between touch and simulated mouse events. Touch events will be detected
* by one instance which moves the target. The simulated mouse events will be fired with
* a delay which causes another target and with that, another instance of this handler.
* last touch was.
*/
__lastTouch : null
},
/**
* Create a new instance
*
* @param target {Element} element on which to listen for native touch events
* @param emitter {qx.event.Emitter?} Event emitter (used if dispatchEvent
* is not supported, e.g. in IE8)
*/
construct : function(target, emitter) {
this.__defaultTarget = target;
this.__emitter = emitter;
this.__eventNames = [];
this.__buttonStates = [];
this.__activeTouches = [];
this._processedFlag = "$$qx" +
this.classname.substr(this.classname.lastIndexOf(".") + 1) +
"Processed";
var engineName = qx.core.Environment.get("engine.name");
var docMode = parseInt(qx.core.Environment.get("browser.documentmode"), 10);
if (engineName == "mshtml" && docMode == 10) {
// listen to native prefixed events and custom unprefixed (see bug #8921)
this.__eventNames = [
"MSPointerDown", "MSPointerMove", "MSPointerUp", "MSPointerCancel", "MSPointerOver", "MSPointerOut",
"pointerdown", "pointermove", "pointerup", "pointercancel", "pointerover", "pointerout"
];
this._initPointerObserver();
} else {
if (qx.core.Environment.get("event.mspointer")) {
this.__nativePointerEvents = true;
}
this.__eventNames = [
"pointerdown", "pointermove", "pointerup", "pointercancel", "pointerover", "pointerout"
];
this._initPointerObserver();
}
if (!qx.core.Environment.get("event.mspointer")) {
if (qx.core.Environment.get("device.touch")) {
this.__eventNames = ["touchstart", "touchend", "touchmove", "touchcancel"];
this._initObserver(this._onTouchEvent);
}
this.__eventNames = [
"mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "contextmenu"
];
this._initObserver(this._onMouseEvent);
}
},
members : {
__defaultTarget : null,
__emitter : null,
__eventNames : null,
__nativePointerEvents : false,
__wrappedListener : null,
__lastButtonState : 0,
__buttonStates : null,
__primaryIdentifier : null,
__activeTouches : null,
_processedFlag : null,
/**
* Adds listeners to native pointer events if supported
*/
_initPointerObserver : function() {
this._initObserver(this._onPointerEvent);
},
/**
* Register native event listeners
* @param callback {Function} listener callback
* @param useEmitter {Boolean} attach listener to Emitter instead of
* native event
*/
_initObserver : function(callback, useEmitter) {
this.__wrappedListener = qx.lang.Function.listener(callback, this);
this.__eventNames.forEach(function(type) {
if (useEmitter && qx.dom.Node.isDocument(this.__defaultTarget)) {
if (!this.__defaultTarget.$$emitter) {
this.__defaultTarget.$$emitter = new qx.event.Emitter();
}
this.__defaultTarget.$$emitter.on(type, this.__wrappedListener);
} else {
qx.bom.Event.addNativeListener(this.__defaultTarget, type, this.__wrappedListener);
}
}.bind(this));
},
/**
* Handler for native pointer events
* @param domEvent {Event} Native DOM event
*/
_onPointerEvent : function(domEvent) {
if (!qx.core.Environment.get("event.mspointer") ||
// workaround for bug #8533
(qx.core.Environment.get("browser.documentmode") === 10 && domEvent.type.toLowerCase().indexOf("ms") == -1)
)
{
return;
}
if (!this.__nativePointerEvents) {
domEvent.stopPropagation();
}
var type = qx.event.handler.PointerCore.MSPOINTER_TO_POINTER_MAPPING[domEvent.type] || domEvent.type;
var target = qx.bom.Event.getTarget(domEvent);
var evt = new qx.event.type.dom.Pointer(type, domEvent);
this._fireEvent(evt, type, target);
},
/**
* Handler for touch events
* @param domEvent {Event} Native DOM event
*/
_onTouchEvent: function(domEvent) {
if (domEvent[this._processedFlag]) {
return;
}
domEvent[this._processedFlag] = true;
var type = qx.event.handler.PointerCore.TOUCH_TO_POINTER_MAPPING[domEvent.type];
var changedTouches = domEvent.changedTouches;
this._determineActiveTouches(domEvent.type, changedTouches);
// Detecting vacuum touches. (Touches which are not active anymore, but did not fire a touchcancel event)
if (domEvent.touches.length < this.__activeTouches.length) {
// Firing pointer cancel for previously active touches.
for (var i = this.__activeTouches.length - 1; i >= 0; i--) {
var cancelEvent = new qx.event.type.dom.Pointer("pointercancel", domEvent, {
identifier: this.__activeTouches[i].identifier,
target: domEvent.target,
pointerType: "touch",
pointerId: this.__activeTouches[i].identifier + 2
});
this._fireEvent(cancelEvent, "pointercancel", domEvent.target);
}
// Reset primary identifier
this.__primaryIdentifier = null;
// cleanup of active touches array.
this.__activeTouches = [];
// Do nothing after pointer cancel.
return;
}
if (domEvent.type == "touchstart" && this.__primaryIdentifier === null) {
this.__primaryIdentifier = changedTouches[0].identifier;
}
for (var i = 0, l = changedTouches.length; i < l; i++) {
var touch = changedTouches[i];
var touchTarget = domEvent.view.document.elementFromPoint(touch.clientX,touch.clientY) || domEvent.target;
var touchProps = {
clientX: touch.clientX,
clientY: touch.clientY,
pageX: touch.pageX,
pageY: touch.pageY,
identifier: touch.identifier,
screenX: touch.screenX,
screenY: touch.screenY,
target: touchTarget,
pointerType: "touch",
pointerId: touch.identifier + 2
};
if (domEvent.type == "touchstart") {
// Fire pointerenter before pointerdown
var overEvt = new qx.event.type.dom.Pointer("pointerover", domEvent, touchProps);
this._fireEvent(overEvt, "pointerover", touchProps.target);
}
if (touch.identifier == this.__primaryIdentifier) {
touchProps.isPrimary = true;
// always simulate left click on touch interactions for primary pointer
touchProps.button = 0;
touchProps.buttons = 1;
qx.event.handler.PointerCore.__lastTouch = {
"x": touch.clientX,
"y": touch.clientY,
"time": new Date().getTime()
};
}
var evt = new qx.event.type.dom.Pointer(type, domEvent, touchProps);
this._fireEvent(evt, type, touchProps.target);
if (domEvent.type == "touchend" || domEvent.type == "touchcancel") {
// Fire pointerout after pointerup
var outEvt = new qx.event.type.dom.Pointer("pointerout", domEvent, touchProps);
// fire on the original target to make sure over / out event are on the same target
this._fireEvent(outEvt, "pointerout", domEvent.target);
if (this.__primaryIdentifier == touch.identifier) {
this.__primaryIdentifier = null;
}
}
}
},
/**
* Handler for touch events
* @param domEvent {Event} Native DOM event
*/
_onMouseEvent : function(domEvent) {
if (domEvent[this._processedFlag]) {
return;
}
domEvent[this._processedFlag] = true;
if (this._isSimulatedMouseEvent(domEvent.clientX, domEvent.clientY)) {
/*
Simulated MouseEvents are fired by browsers directly after TouchEvents
for improving compatibility. They should not trigger PointerEvents.
*/
return;
}
if (domEvent.type == "mousedown") {
this.__buttonStates[domEvent.which] = 1;
} else if (domEvent.type == "mouseup") {
if (qx.core.Environment.get("os.name") == "osx" && qx.core.Environment.get("engine.name") == "gecko") {
if (this.__buttonStates[domEvent.which] != 1 && domEvent.ctrlKey) {
this.__buttonStates[1] = 0;
}
}
this.__buttonStates[domEvent.which] = 0;
}
var type = qx.event.handler.PointerCore.MOUSE_TO_POINTER_MAPPING[domEvent.type];
var target = qx.bom.Event.getTarget(domEvent);
var buttonsPressed = qx.lang.Array.sum(this.__buttonStates);
var mouseProps = {pointerType : "mouse", pointerId: 1};
// if the button state changes but not from or to zero
if (this.__lastButtonState != buttonsPressed && buttonsPressed !== 0 && this.__lastButtonState !== 0) {
var moveEvt = new qx.event.type.dom.Pointer("pointermove", domEvent, mouseProps);
this._fireEvent(moveEvt, "pointermove", target);
}
this.__lastButtonState = buttonsPressed;
// pointerdown should only trigger form the first pressed button.
if (domEvent.type == "mousedown" && buttonsPressed > 1) {
return;
}
// pointerup should only trigger if user releases all buttons.
if (domEvent.type == "mouseup" && buttonsPressed > 0) {
return;
}
if (domEvent.type == "contextmenu") {
this.__buttonStates[domEvent.which] = 0;
return;
}
var evt = new qx.event.type.dom.Pointer(type, domEvent, mouseProps);
this._fireEvent(evt, type, target);
},
/**
* Determines the current active touches.
* @param type {String} the DOM event type.
* @param changedTouches {Array} the current changed touches.
*/
_determineActiveTouches: function(type, changedTouches) {
if (type == "touchstart") {
for (var i = 0; i < changedTouches.length; i++) {
this.__activeTouches.push(changedTouches[i]);
}
} else if (type == "touchend" || type == "touchcancel") {
var updatedActiveTouches = [];
for (var i = 0; i < this.__activeTouches.length; i++) {
var add = true;
for (var j = 0; j < changedTouches.length; j++) {
if (this.__activeTouches[i].identifier == changedTouches[j].identifier) {
add = false;
break;
}
}
if (add) {
updatedActiveTouches.push(this.__activeTouches[i]);
}
}
this.__activeTouches = updatedActiveTouches;
}
},
/**
* Detects whether the given MouseEvent position is identical to the previously fired TouchEvent position.
* If <code>true</code> the corresponding event can be identified as simulated.
* @param x {Integer} current mouse x
* @param y {Integer} current mouse y
* @return {Boolean} <code>true</code> if passed mouse position is a synthetic MouseEvent.
*/
_isSimulatedMouseEvent: function(x, y) {
var touch = qx.event.handler.PointerCore.__lastTouch;
if (touch) {
var timeSinceTouch = new Date().getTime() - touch.time;
var dist = qx.event.handler.PointerCore.SIM_MOUSE_DISTANCE;
var distX = Math.abs(x - qx.event.handler.PointerCore.__lastTouch.x);
var distY = Math.abs(y - qx.event.handler.PointerCore.__lastTouch.y);
if (timeSinceTouch < qx.event.handler.PointerCore.SIM_MOUSE_DELAY) {
if (distX < dist || distY < dist) {
return true;
}
}
}
return false;
},
/**
* Removes native pointer event listeners.
*/
_stopObserver : function() {
for (var i = 0; i < this.__eventNames.length; i++) {
qx.bom.Event.removeNativeListener(this.__defaultTarget, this.__eventNames[i], this.__wrappedListener);
}
},
/**
* Fire a touch event with the given parameters
*
* @param domEvent {Event} DOM event
* @param type {String ? null} type of the event
* @param target {Element ? null} event target
* @return {qx.Promise?} a promise, if one was returned by event handlers
*/
_fireEvent : function(domEvent, type, target)
{
target = target || domEvent.target;
type = type || domEvent.type;
var gestureEvent;
if ((domEvent.pointerType !== "mouse" ||
domEvent.button <= qx.event.handler.PointerCore.LEFT_BUTTON) &&
(type == "pointerdown" || type == "pointerup" || type == "pointermove"))
{
gestureEvent = new qx.event.type.dom.Pointer(
qx.event.handler.PointerCore.POINTER_TO_GESTURE_MAPPING[type],
domEvent);
qx.event.type.dom.Pointer.normalize(gestureEvent);
try {
gestureEvent.srcElement = target;
}catch(ex) {
// Nothing - strict mode prevents writing to read only properties
}
}
if (qx.core.Environment.get("event.dispatchevent")) {
var tracker = {};
if (!this.__nativePointerEvents) {
qx.event.Utils.then(tracker, function() {
return target.dispatchEvent(domEvent);
});
}
if (gestureEvent) {
qx.event.Utils.then(tracker, function() {
return target.dispatchEvent(gestureEvent);
});
}
return tracker.promise;
} else {
// ensure compatibility with native events for IE8
try {
domEvent.srcElement = target;
}catch(ex) {
// Nothing - strict mode prevents writing to read only properties
}
while (target) {
if (target.$$emitter) {
domEvent.currentTarget = target;
if (!domEvent._stopped) {
target.$$emitter.emit(type, domEvent);
}
if (gestureEvent && !gestureEvent._stopped) {
gestureEvent.currentTarget = target;
target.$$emitter.emit(gestureEvent.type, gestureEvent);
}
}
target = target.parentNode;
}
}
},
/**
* Dispose this object
*/
dispose : function() {
this._stopObserver();
this.__defaultTarget = this.__emitter = null;
}
}
});