From 2c8af72f5ed4cff6415f4dfffc8710d871d3380c Mon Sep 17 00:00:00 2001 From: Mark Salsbery Date: Tue, 1 Apr 2014 09:32:04 -0700 Subject: [PATCH] Multi-Touch MouseTracker Fixes Removed zoom on click for touch. Added an isPrimary flag to gesture points. Fixed documentation error and updated documentation and comments. Implemented the stopHandler for mouse events. --- src/mousetracker.js | 368 ++++++++++++++------------------------------ src/viewer.js | 2 +- 2 files changed, 119 insertions(+), 251 deletions(-) diff --git a/src/mousetracker.js b/src/mousetracker.js index 4134a1da..c2aa48d8 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -34,7 +34,7 @@ (function ( $ ) { - // is any button currently being pressed while mouse events occur + // is any button currently being pressed while pointer events occur var IS_BUTTON_DOWN = false, // is any tracker currently capturing? //IS_CAPTURING = false, @@ -48,7 +48,7 @@ /** * @class MouseTracker - * @classdesc Provides simplified handling of common mouse, touch, and keyboard + * @classdesc Provides simplified handling of common pointing device (mouse, touch, pen) and keyboard * events on a specific element, like 'enter', 'exit', 'press', 'release', * 'scroll', 'click', and 'drag'. * @@ -59,31 +59,31 @@ * the original positional arguments 'element', 'clickTimeThreshold', * and 'clickDistThreshold' in that order. * @param {Element|String} options.element - * A reference to an element or an element id for which the mouse/touch/key + * A reference to an element or an element id for which the pointer/key * events will be monitored. * @param {Number} options.clickTimeThreshold - * The number of milliseconds within which multiple mouse clicks + * The number of milliseconds within which multiple pointer clicks * will be treated as a single event. * @param {Number} options.clickDistThreshold - * The distance between mouse click within multiple mouse clicks + * The distance between pointer click within multiple pointer clicks * will be treated as a single event. * @param {Number} [options.stopDelay=50] - * The number of milliseconds without mouse move before the mouse stop + * The number of milliseconds without pointer move before the stop * event is fired. * @param {OpenSeadragon.EventHandler} [options.enterHandler=null] - * An optional handler for mouse enter. + * An optional handler for pointer enter. * @param {OpenSeadragon.EventHandler} [options.exitHandler=null] - * An optional handler for mouse exit. + * An optional handler for pointer exit. * @param {OpenSeadragon.EventHandler} [options.pressHandler=null] - * An optional handler for mouse press. + * An optional handler for pointer press. * @param {OpenSeadragon.EventHandler} [options.releaseHandler=null] - * An optional handler for mouse release. + * An optional handler for pointer release. * @param {OpenSeadragon.EventHandler} [options.moveHandler=null] - * An optional handler for mouse move. + * An optional handler for pointer move. * @param {OpenSeadragon.EventHandler} [options.scrollHandler=null] - * An optional handler for mouse scroll. + * An optional handler for mouse wheel scroll. * @param {OpenSeadragon.EventHandler} [options.clickHandler=null] - * An optional handler for mouse click. + * An optional handler for pointer click. * @param {OpenSeadragon.EventHandler} [options.dragHandler=null] * An optional handler for the drag gesture. * @param {OpenSeadragon.EventHandler} [options.dragEndHandler=null] @@ -113,19 +113,19 @@ this.hash = Math.random(); // An unique hash for this tracker. /** - * The element for which mouse/touch/key events are being monitored. + * The element for which pointer events are being monitored. * @member {Element} element * @memberof OpenSeadragon.MouseTracker# */ this.element = $.getElement( options.element ); /** - * The number of milliseconds within which mutliple mouse clicks will be treated as a single event. + * The number of milliseconds within which mutliple pointer clicks will be treated as a single event. * @member {Number} clickTimeThreshold * @memberof OpenSeadragon.MouseTracker# */ this.clickTimeThreshold = options.clickTimeThreshold; /** - * The distance between mouse click within multiple mouse clicks will be treated as a single event. + * The distance between pointer click within multiple pointer clicks will be treated as a single event. * @member {Number} clickDistThreshold * @memberof OpenSeadragon.MouseTracker# */ @@ -154,9 +154,9 @@ /** * @private * @property {Boolean} tracking - * Are we currently tracking mouse events. + * Are we currently tracking pointer events. * @property {Boolean} capturing - * Are we curruently capturing mouse events. + * Are we curruently capturing mouse events (legacy mouse events only). */ THIS[ this.hash ] = { click: function ( event ) { onClick( _this, event ); }, @@ -222,7 +222,7 @@ $.MouseTracker.prototype = /** @lends OpenSeadragon.MouseTracker.prototype */{ /** - * Clean up any events or objects created by the mouse tracker. + * Clean up any events or objects created by the tracker. * @function */ destroy: function () { @@ -598,14 +598,13 @@ /** + * Provides continuous computation of velocity (speed and direction) of active pointers. + * This is a singleton, used by all MouseTracker instances. Currently it is extremely unlikely there will ever be more than + * two active gesture pointers at a time. + * * @member gesturePointVelocityTracker * @memberof OpenSeadragon.MouseTracker * @private - * - * Provides continuous computation of velocity (speed and direction) of active pointers. - * - * This is a singleton, used by all MouseTracker instances. Currently it is extremely unlikely there will ever be more than - * two active gesture pointers at a time. */ $.MouseTracker.gesturePointVelocityTracker = (function () { var trackerPoints = [], @@ -754,10 +753,6 @@ } -//******************************************************************************************************************************************* -//** Utility Types/Classes - - /** * Represents a point of contact on the screen made by a mouse cursor, pen, touch, or other pointing device. * @@ -768,14 +763,16 @@ * Identifier unique from all other active GesturePoints for a given pointer device. * @property {String} type * The pointer device type: "mouse", "touch", "pen", or "". + * @property {Boolean} isPrimary + * True if the gesture point is a master pointer amongst the set of active pointers for each pointer type. True for mouse and primary (first) touch/pen pointers. * @property {Boolean} insideElementPressed - * True if mouse button pressed or contact point initiated inside the screen area of the tracked element. + * True if button pressed or contact point initiated inside the screen area of the tracked element. * @property {Boolean} insideElement - * True if mouse cursor or contact point is currently inside the bounds of the tracked element. + * True if pointer or contact point is currently inside the bounds of the tracked element. * @property {Number} speed - * Continuously computed speed, in pixels per second. + * Current computed speed, in pixels per second. * @property {Number} direction - * Continuously computed direction, in radians. Only valid if speed > 0. + * Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0. * @property {OpenSeadragon.Point} startPos * The initial pointer position, relative to the page including any scrolling. * @property {Number} startTime @@ -791,10 +788,11 @@ */ - /** + /*** * @class GesturePointList * @classdesc Provides an abstraction for a set of {@link OpenSeadragon.MouseTracker.GesturePoint} objects. * @memberof OpenSeadragon.MouseTracker + * @private */ $.MouseTracker.GesturePointList = function () { this._gPoints = []; @@ -838,6 +836,18 @@ } return this._gPoints.length; }, + /** + * @function + * @param {Number} index - The index of the gesture point to retrieve from the list. + * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point at the given index, or null if not found. + */ + getByIndex: function ( index ) { + if ( index < this._gPoints.length) { + return this._gPoints[ index ]; + } + + return null; + }, /** * @function * @param {Number} id - The id of the gesture point to retrieve from the list. @@ -852,16 +862,26 @@ } } return null; + }, + /** + * @function + * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The primary gesture point in the list, or null if not found. + */ + getPrimary: function ( id ) { + var i, + len = this._gPoints.length; + for ( i = 0; i < len; i++ ) { + if ( this._gPoints[ i ].isPrimary ) { + return this._gPoints[ i ]; + } + } + return null; } }; -//******************************************************************************************************************************************* -//** Utility Functions - - /** - * Starts tracking mouse events on this element. + * Starts tracking pointer events on this element. * @private * @inner */ @@ -886,7 +906,7 @@ } /** - * Stops tracking mouse events on this element. + * Stops tracking pointer events on this element. * @private * @inner */ @@ -921,7 +941,7 @@ } /** - * Begin capturing mouse events on this element. + * Begin capturing mouse events on this element (legacy mouse events only). * @private * @inner */ @@ -968,7 +988,7 @@ /** - * Stop capturing mouse events on this element. + * Stop capturing mouse events on this element (legacy mouse events only). * @private * @inner */ @@ -1118,10 +1138,6 @@ } -//******************************************************************************************************************************************* -//** DOM EVent Handlers - - /** * @private * @inner @@ -1265,6 +1281,7 @@ gPoint = { id: $.MouseTracker.mousePointerId, type: 'mouse', + isPrimary: true, insideElement: true, currentPos: getMouseAbsolute( event ), currentTime: $.now() @@ -1286,6 +1303,7 @@ gPoint = { id: $.MouseTracker.mousePointerId, type: 'mouse', + isPrimary: true, insideElement: false, currentPos: getMouseAbsolute( event ), currentTime: $.now() @@ -1311,6 +1329,7 @@ gPoint = { id: $.MouseTracker.mousePointerId, type: 'mouse', + isPrimary: true, currentPos: getMouseAbsolute( event ), currentTime: $.now() }; @@ -1495,6 +1514,8 @@ function onTouchStart( tracker, event ) { var delegate = THIS[ tracker.hash ], time, + primaryPoint, + primaryId, gPoint, i, touchCount = event.changedTouches.length, @@ -1513,10 +1534,19 @@ updatePointersOver( tracker, event, [ gPoint ] ); } + primaryPoint = delegate.touchPoints.getPrimary(); + if ( primaryPoint ) { + primaryId = primaryPoint.id; + } + else { + primaryId = event.changedTouches[ 0 ].identifier; + } + for ( i = 0; i < touchCount; i++ ) { gPoints.push( { id: event.changedTouches[ i ].identifier, type: 'touch', + isPrimary: event.changedTouches[ i ].identifier === primaryId, currentPos: getMouseAbsolute( event.changedTouches[ i ] ), currentTime: time } ); @@ -1539,6 +1569,7 @@ function onTouchEnd( tracker, event ) { var delegate = THIS[ tracker.hash ], time, + primaryPoint, gPoint, i, touchCount = event.changedTouches.length, @@ -1557,6 +1588,14 @@ removePointers( tracker, event, gPoints ); + primaryPoint = delegate.touchPoints.getPrimary(); + if ( !primaryPoint ) { + primaryPoint = delegate.touchPoints.getByIndex( 0 ); + if ( primaryPoint ) { + primaryPoint.isPrimary = true; + } + } + if ( !$.MouseTracker.haveTouchEnter && touchCount > 0 && delegate.touchPoints.getLength() === 0 ) { gPoint = { id: event.changedTouches[ 0 ].identifier, @@ -1656,6 +1695,7 @@ gPoint = { id: event.pointerId, type: getPointerType( event ), + isPrimary: event.isPrimary, insideElement: true, currentPos: getMouseAbsolute( event ), currentTime: $.now() @@ -1675,6 +1715,7 @@ gPoint = { id: event.pointerId, type: getPointerType( event ), + isPrimary: event.isPrimary, insideElement: false, currentPos: getMouseAbsolute( event ), currentTime: $.now() @@ -1705,6 +1746,7 @@ gPoint = { id: event.pointerId, type: getPointerType( event ), + isPrimary: event.isPrimary, currentPos: getMouseAbsolute( event ), currentTime: $.now() }; @@ -1740,6 +1782,7 @@ gPoint = { id: event.pointerId, type: getPointerType( event ), + isPrimary: event.isPrimary, currentPos: getMouseAbsolute( event ), currentTime: $.now() }; @@ -1765,6 +1808,7 @@ gPoint = { id: event.pointerId, type: getPointerType( event ), + isPrimary: event.isPrimary, currentPos: getMouseAbsolute( event ), currentTime: $.now() }; @@ -1789,16 +1833,13 @@ gPoint = { id: event.pointerId, type: getPointerType( event ), + isPrimary: event.isPrimary, }; cancelPointers( tracker, event, [ gPoint ] ); } -//******************************************************************************************************************************************* -//** Event Processing Functions - - /** * Handles 'wheel' events. * The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()). @@ -1850,6 +1891,7 @@ gPoint = { id: $.MouseTracker.mousePointerId, type: 'mouse', + isPrimary: true, currentPos: getMouseAbsolute( event ), currentTime: $.now() }; @@ -1874,6 +1916,7 @@ gPoint = { id: $.MouseTracker.mousePointerId, type: 'mouse', + isPrimary: true, currentPos: getMouseAbsolute( event ), currentTime: $.now() }; @@ -1886,13 +1929,13 @@ * @private * @inner */ - function handlePointerStop( tracker, originalMoveEvent ) { + function handlePointerStop( tracker, originalMoveEvent, pointerType ) { if ( tracker.stopHandler ) { tracker.stopHandler( { eventSource: tracker, - pointerType: '', + pointerType: pointerType, position: getMouseRelative( originalMoveEvent, tracker.element ), - isTouchEvent: false, + isTouchEvent: pointerType === 'touch', originalEvent: originalMoveEvent, preventDefaultAction: false, userData: tracker.userData @@ -2107,24 +2150,25 @@ updateGPoint.currentPos = curGPoint.currentPos; updateGPoint.currentTime = curGPoint.currentTime; } + } - //if ( tracker.stopHandler ) { - // clearTimeout( tracker.stopTimeOut ); - // tracker.stopTimeOut = setTimeout( function() { - // handlePointerStop( tracker, event ); - // }, tracker.stopDelay ); - //} + // Stop (mouse only) + if ( gPoints[ 0 ].type === 'mouse' && tracker.stopHandler ) { + clearTimeout( tracker.stopTimeOut ); + tracker.stopTimeOut = setTimeout( function() { + handlePointerStop( tracker, event, gPoints[ 0 ].type ); + }, tracker.stopDelay ); } if ( pointsListLength === 0 ) { - // Move (no contacts) + // Move (no contacts, mouse or other hover-capable device) if ( tracker.moveHandler ) { propagate = tracker.moveHandler( { eventSource: tracker, - pointerType: curGPoint.type, - position: getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), - isTouchEvent: curGPoint.type === 'touch', + pointerType: gPoints[ 0 ].type, + position: getPointRelativeToAbsolute( gPoints[ 0 ].currentPos, tracker.element ), + isTouchEvent: gPoints[ 0 ].type === 'touch', originalEvent: event, preventDefaultAction: false, userData: tracker.userData @@ -2364,199 +2408,23 @@ } -//******************************************************************************************************************************************* -//******************************************************************************************************************************************* - - - //function handlePointerOver( tracker, event, position ) { - // var delegate = THIS[ tracker.hash ], - // propagate; - - // //if ( !isTouch ) { - // // if ( $.Browser.vendor == $.BROWSERS.IE && - // // $.Browser.version < 9 && - // // delegate.capturing && - // // !isChild( event.srcElement, tracker.element ) ) { - - // // triggerOthers( tracker, onMouseOver, event, isTouch ); - // // } - - // // var to = event.target ? - // // event.target : - // // event.srcElement, - // // from = event.relatedTarget ? - // // event.relatedTarget : - // // event.fromElement; - - // // if ( !isChild( tracker.element, to ) || - // // isChild( tracker.element, from ) ) { - // // return; - // // } - // //} - - // //delegate.insideElement = true; - - // if ( tracker.enterHandler ) { - // propagate = tracker.enterHandler( - // { - // eventSource: tracker, - // position: position, - // insideElementPressed: delegate.insideElementPressed, - // buttonDownAny: IS_BUTTON_DOWN, - // isTouchEvent: false,//isTouch, - // originalEvent: event, - // preventDefaultAction: false, - // userData: tracker.userData - // } - // ); - // if ( propagate === false ) { - // $.cancelEvent( event ); - // } - // } - //} - - - //function handlePointerOut( tracker, event, position ) { - // var delegate = THIS[ tracker.hash ], - // propagate; - - // //if ( !isTouch ) { - // // if ( $.Browser.vendor == $.BROWSERS.IE && - // // $.Browser.version < 9 && - // // delegate.capturing && - // // !isChild( event.srcElement, tracker.element ) ) { - - // // triggerOthers( tracker, onMouseOut, event, isTouch ); - - // // } - - // // var from = event.target ? - // // event.target : - // // event.srcElement, - // // to = event.relatedTarget ? - // // event.relatedTarget : - // // event.toElement; - - // // if ( !isChild( tracker.element, from ) || - // // isChild( tracker.element, to ) ) { - // // return; - // // } - // //} - - // //delegate.insideElement = false; - - // if ( tracker.exitHandler ) { - // propagate = tracker.exitHandler( - // { - // eventSource: tracker, - // position: position, - // insideElementPressed: delegate.insideElementPressed, - // buttonDownAny: IS_BUTTON_DOWN, - // isTouchEvent: false,//isTouch, - // originalEvent: event, - // preventDefaultAction: false, - // userData: tracker.userData - // } - // ); - - // if ( propagate === false ) { - // $.cancelEvent( event ); - // } - // } - //} - - ///** - // * @private - // * @inner - // */ - //function handlePointerMove( tracker, event ) { - // var propagate; - // if ( tracker.moveHandler ) { - // propagate = tracker.moveHandler( - // { - // eventSource: tracker, - // position: getMouseRelative( event, tracker.element ), - // isTouchEvent: false, - // originalEvent: event, - // preventDefaultAction: false, - // userData: tracker.userData - // } - // ); - // if ( propagate === false ) { - // $.cancelEvent( event ); + // * @private + // * @inner + // * Returns true if elementB is a child node of elementA, or if they're equal. + // */ + //function isChild( elementA, elementB ) { + // var body = document.body; + // while ( elementB && elementA != elementB && body != elementB ) { + // try { + // elementB = elementB.parentNode; + // } catch ( e ) { + // return false; // } // } - // if ( tracker.stopHandler ) { - // clearTimeout( tracker.stopTimeOut ); - // tracker.stopTimeOut = setTimeout( function() { - // handlePointerStop( tracker, event ); - // }, tracker.stopDelay ); - // } + // return elementA == elementB; //} - // - ///** - // * @private - // * @inner - // */ - //function handleMouseClick( tracker, event, isTouch ) { - // var delegate = THIS[ tracker.hash ], - // propagate; - - // isTouch = isTouch || false; - - // event = $.getEvent( event ); - - // var eventOrTouchPoint = isTouch ? event.changedTouches[ 0 ] : event; - - // if ( event.button == 2 ) { - // return; - // } - - // var time = $.now() - delegate.lastMouseDownTime, - // point = getMouseAbsolute( eventOrTouchPoint ), - // distance = delegate.lastMouseDownPoint.distanceTo( point ), - // quick = time <= tracker.clickTimeThreshold && - // distance <= tracker.clickDistThreshold; - - // if ( tracker.clickHandler ) { - // propagate = tracker.clickHandler( - // { - // eventSource: tracker, - // position: getMouseRelative( eventOrTouchPoint, tracker.element ), - // quick: quick, - // shift: event.shiftKey, - // isTouchEvent: isTouch, - // originalEvent: event, - // preventDefaultAction: false, - // userData: tracker.userData - // } - // ); - // if ( propagate === false ) { - // $.cancelEvent( event ); - // } - // } - //} - - - /** - * @private - * @inner - * Returns true if elementB is a child node of elementA, or if they're equal. - */ - function isChild( elementA, elementB ) { - var body = document.body; - while ( elementB && elementA != elementB && body != elementB ) { - try { - elementB = elementB.parentNode; - } catch ( e ) { - return false; - } - } - return elementA == elementB; - } - /** * @private * @inner diff --git a/src/viewer.js b/src/viewer.js index 97b72b82..7e834f2b 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -2207,7 +2207,7 @@ function onBlur(){ function onCanvasClick( event ) { var zoomPerClick, factor; - if ( !event.preventDefaultAction && this.viewport && event.quick ) { // ignore clicks where mouse moved + if ( event.pointerType !== 'touch' && !event.preventDefaultAction && this.viewport && event.quick ) { zoomPerClick = this.zoomPerClick; factor = event.shift ? 1.0 / zoomPerClick : zoomPerClick; this.viewport.zoomBy(