UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

492 lines (426 loc) 14.7 kB
/* ************************************************************************ 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; } } });