@qooxdoo/framework
Version:
The JS Framework for Coders
492 lines (426 loc) • 14.7 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2012 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:
* Martin Wittemann (martinwittemann)
* Tino Butz (tbtz)
* Christian Hagendorn (chris_schmidt)
* Daniel Wagner (danielwagner)
************************************************************************ */
/**
* Listens for native touch events and fires composite events like "tap" and
* "swipe"
*
* @ignore(qx.event.*)
*/
qx.Bootstrap.define("qx.event.handler.TouchCore", {
extend: Object,
implement: [qx.core.IDisposable],
statics: {
/** @type {Integer} The maximum distance of a tap. Only if the x or y distance of
* the performed tap is less or equal the value of this constant, a tap
* event is fired.
*/
TAP_MAX_DISTANCE: qx.core.Environment.get("os.name") != "android" ? 10 : 40,
/** @type {Map} The direction of a swipe relative to the axis */
SWIPE_DIRECTION: {
x: ["left", "right"],
y: ["up", "down"]
},
/** @type {Integer} The minimum distance of a swipe. Only if the x or y distance
* of the performed swipe is greater as or equal the value of this
* constant, a swipe event is fired.
*/
SWIPE_MIN_DISTANCE:
qx.core.Environment.get("os.name") != "android" ? 11 : 41,
/** @type {Integer} The minimum velocity of a swipe. Only if the velocity of the
* performed swipe is greater as or equal the value of this constant, a
* swipe event is fired.
*/
SWIPE_MIN_VELOCITY: 0,
/**
* @type {Integer} The time delta in milliseconds to fire a long tap event.
*/
LONGTAP_TIME: qx.core.Environment.get("device.touch") ? 500 : 99999
},
/**
* Create a new instance
*
* @param target {Element} element on which to listen for native touch events
* @param emitter {qx.event.Emitter} Event emitter object
*/
construct(target, emitter) {
this.__target = target;
this.__emitter = emitter;
this._initTouchObserver();
this.__pointers = [];
this.__touchStartPosition = {};
},
members: {
__target: null,
__emitter: null,
__onTouchEventWrapper: null,
__originalTarget: null,
__touchStartPosition: null,
__startTime: null,
__beginScalingDistance: null,
__beginRotation: null,
__pointers: null,
__touchEventNames: null,
/*
---------------------------------------------------------------------------
OBSERVER INIT
---------------------------------------------------------------------------
*/
/**
* Initializes the native touch event listeners.
*/
_initTouchObserver() {
this.__onTouchEventWrapper = qx.lang.Function.listener(
this._onTouchEvent,
this
);
this.__touchEventNames = [
"touchstart",
"touchmove",
"touchend",
"touchcancel"
];
if (qx.core.Environment.get("event.mspointer")) {
var engineVersion = parseInt(
qx.core.Environment.get("engine.version"),
10
);
if (engineVersion == 10) {
// IE 10
this.__touchEventNames = [
"MSPointerDown",
"MSPointerMove",
"MSPointerUp",
"MSPointerCancel"
];
} else {
// IE 11+
this.__touchEventNames = [
"pointerdown",
"pointermove",
"pointerup",
"pointercancel"
];
}
}
for (var i = 0; i < this.__touchEventNames.length; i++) {
qx.bom.Event.addNativeListener(
this.__target,
this.__touchEventNames[i],
this.__onTouchEventWrapper
);
}
},
/*
---------------------------------------------------------------------------
OBSERVER STOP
---------------------------------------------------------------------------
*/
/**
* Disconnects the native touch event listeners.
*/
_stopTouchObserver() {
for (var i = 0; i < this.__touchEventNames.length; i++) {
qx.bom.Event.removeNativeListener(
this.__target,
this.__touchEventNames[i],
this.__onTouchEventWrapper
);
}
},
/*
---------------------------------------------------------------------------
NATIVE EVENT OBSERVERS
---------------------------------------------------------------------------
*/
/**
* Handler for native touch events.
*
* @param domEvent {Event} The touch event from the browser.
*/
_onTouchEvent(domEvent) {
this._commonTouchEventHandler(domEvent);
},
/**
* Calculates the scaling distance between two touches.
* @param touch0 {Event} The touch event from the browser.
* @param touch1 {Event} The touch event from the browser.
* @return {Number} the calculated distance.
*/
_getScalingDistance(touch0, touch1) {
return Math.sqrt(
Math.pow(touch0.pageX - touch1.pageX, 2) +
Math.pow(touch0.pageY - touch1.pageY, 2)
);
},
/**
* Calculates the rotation between two touches.
* @param touch0 {Event} The touch event from the browser.
* @param touch1 {Event} The touch event from the browser.
* @return {Number} the calculated rotation.
*/
_getRotationAngle(touch0, touch1) {
var x = touch0.pageX - touch1.pageX;
var y = touch0.pageY - touch1.pageY;
return (Math.atan2(y, x) * 180) / Math.PI;
},
/**
* Calculates the delta of the touch position relative to its position when <code>touchstart/code> event occurred.
* @param touches {Array} an array with the current active touches, provided by <code>touchmove/code> event.
* @return {Array} an array containing objects with the calculated delta as <code>x</code>,
* <code>y</code> and the identifier of the corresponding touch.
*/
_calcTouchesDelta(touches) {
var delta = [];
for (var i = 0; i < touches.length; i++) {
delta.push(this._calcSingleTouchDelta(touches[i]));
}
return delta;
},
/**
* Calculates the delta of one single touch position relative to its position when <code>touchstart/code> event occurred.
* @param touch {Event} the current active touch, provided by <code>touchmove/code> event.
* @return {Map} a map containing deltaX as <code>x</code>, deltaY as <code>y</code>, the direction of the movement as <code>axis</code> and the touch identifier as <code>identifier</code>.
*/
_calcSingleTouchDelta(touch) {
if (this.__touchStartPosition.hasOwnProperty(touch.identifier)) {
var touchStartPosition = this.__touchStartPosition[touch.identifier];
var deltaX = Math.floor(touch.clientX - touchStartPosition[0]);
var deltaY = Math.floor(touch.clientY - touchStartPosition[1]);
var axis = "x";
if (Math.abs(deltaX / deltaY) < 1) {
axis = "y";
}
return {
x: deltaX,
y: deltaY,
axis: axis,
identifier: touch.identifier
};
} else {
return {
x: 0,
y: 0,
axis: null,
identifier: touch.identifier
};
}
},
/**
* Called by an event handler.
*
* @param domEvent {Event} DOM event
* @param type {String ? null} type of the event
*/
_commonTouchEventHandler(domEvent, type) {
var type = type || domEvent.type;
if (qx.core.Environment.get("event.mspointer")) {
type = this._mapPointerEvent(type);
var touches = this._detectTouchesByPointer(domEvent, type);
domEvent.changedTouches = touches;
domEvent.targetTouches = touches;
domEvent.touches = touches;
}
domEvent.delta = [];
if (type == "touchstart") {
this.__originalTarget = this._getTarget(domEvent);
if (domEvent.touches && domEvent.touches.length > 1) {
this.__beginScalingDistance = this._getScalingDistance(
domEvent.touches[0],
domEvent.touches[1]
);
this.__beginRotation = this._getRotationAngle(
domEvent.touches[0],
domEvent.touches[1]
);
}
for (var i = 0; i < domEvent.changedTouches.length; i++) {
var touch = domEvent.changedTouches[i];
this.__touchStartPosition[touch.identifier] = [
touch.clientX,
touch.clientY
];
}
}
if (type == "touchmove") {
// Polyfill for scale
if (
typeof domEvent.scale == "undefined" &&
domEvent.targetTouches.length > 1
) {
var currentScalingDistance = this._getScalingDistance(
domEvent.targetTouches[0],
domEvent.targetTouches[1]
);
domEvent.scale = currentScalingDistance / this.__beginScalingDistance;
}
// Polyfill for rotation
if (
(typeof domEvent.rotation == "undefined" ||
qx.core.Environment.get("event.mspointer")) &&
domEvent.targetTouches.length > 1
) {
var currentRotation = this._getRotationAngle(
domEvent.targetTouches[0],
domEvent.targetTouches[1]
);
domEvent._rotation = currentRotation - this.__beginRotation;
}
domEvent.delta = this._calcTouchesDelta(domEvent.targetTouches);
}
this._fireEvent(domEvent, type, this.__originalTarget);
if (qx.core.Environment.get("event.mspointer")) {
if (type == "touchend" || type == "touchcancel") {
delete this.__pointers[domEvent.pointerId];
}
}
if (
(type == "touchend" || type == "touchcancel") &&
domEvent.changedTouches[0]
) {
delete this.__touchStartPosition[domEvent.changedTouches[0].identifier];
}
},
/**
* Creates an array with all current used touches out of multiple serial pointer events.
* Needed because pointerEvents do not provide a touch list.
* @param domEvent {Event} DOM event
* @param type {String ? null} type of the event
* @return {Array} touch list array.
*/
_detectTouchesByPointer(domEvent, type) {
var touches = [];
if (type == "touchstart") {
this.__pointers[domEvent.pointerId] = domEvent;
} else if (type == "touchmove") {
this.__pointers[domEvent.pointerId] = domEvent;
}
for (var pointerId in this.__pointers) {
var pointer = this.__pointers[pointerId];
touches.push(pointer);
}
return touches;
},
/**
* Maps a pointer event type to the corresponding touch event type.
* @param type {String} the event type to parse.
* @return {String} the parsed event name.
*/
_mapPointerEvent(type) {
type = type.toLowerCase();
if (type.indexOf("pointerdown") !== -1) {
return "touchstart";
} else if (type.indexOf("pointerup") !== -1) {
return "touchend";
} else if (type.indexOf("pointermove") !== -1) {
return "touchmove";
} else if (type.indexOf("pointercancel") !== -1) {
return "touchcancel";
}
return type;
},
/**
* Return the target of the event.
*
* @param domEvent {Event} DOM event
* @return {Element} Event target
*/
_getTarget(domEvent) {
var target = qx.bom.Event.getTarget(domEvent);
// Text node. Fix Safari Bug, see http://www.quirksmode.org/js/events_properties.html
if (qx.core.Environment.get("engine.name") == "webkit") {
if (target && target.nodeType == 3) {
target = target.parentNode;
}
} else if (
qx.core.Environment.get("engine.name") == "mshtml" &&
qx.core.Environment.get("browser.documentmode") < 11
) {
// Fix for IE10 and pointer-events:none
//
// Changed the condition above to match exactly those browsers
// for which the fix was intended
// See: https://github.com/qooxdoo/qooxdoo/issues/9481
//
var targetForIE = this.__evaluateTarget(domEvent);
if (targetForIE) {
target = targetForIE;
}
}
return target;
},
/**
* This method fixes "pointer-events:none" for Internet Explorer 10.
* Checks which elements are placed to position x/y and traverses the array
* till one element has no "pointer-events:none" inside its style attribute.
* @param domEvent {Event} DOM event
* @return {Element|null} Event target
*/
__evaluateTarget(domEvent) {
var clientX = null;
var clientY = null;
if (domEvent && domEvent.touches && domEvent.touches.length !== 0) {
clientX = domEvent.touches[0].clientX;
clientY = domEvent.touches[0].clientY;
}
// Retrieve an array with elements on point X/Y.
var hitTargets = document.msElementsFromPoint(clientX, clientY);
if (hitTargets) {
// Traverse this array for the elements which has no pointer-events:none inside.
for (var i = 0; i < hitTargets.length; i++) {
var currentTarget = hitTargets[i];
var pointerEvents = qx.bom.element.Style.get(
currentTarget,
"pointer-events",
3
);
if (pointerEvents != "none") {
return currentTarget;
}
}
}
return null;
},
/**
* 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
*/
_fireEvent(domEvent, type, target) {
if (!target) {
target = this._getTarget(domEvent);
}
var type = type || domEvent.type;
if (target && target.nodeType && this.__emitter) {
this.__emitter.emit(type, domEvent);
}
},
/**
* Dispose this object
*/
dispose() {
this._stopTouchObserver();
this.__originalTarget =
this.__target =
this.__touchEventNames =
this.__pointers =
this.__emitter =
this.__beginScalingDistance =
this.__beginRotation =
null;
}
}
});