diff --git a/changelog.txt b/changelog.txt index 7fe59d18..e8dbe41b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -7,9 +7,13 @@ OPENSEADRAGON CHANGELOG * The drawer element is no longer accessible via viewer.canvas.firstChild but via viewer.drawersContainer.firstChild or viewer.drawer.canvas. * The overlays elements are no longer accessible via viewer.canvas.childNodes but via viewer.overlaysContainer.childNodes or viewer.currentOverlays[i].element. * BREAKING CHANGE: Pseudo full screen mode on IE<11 using activex has been dropped. OpenSeadragon will run in full page if full screen mode is requested. +* BREAKING CHANGE: MouseTracker touch pinch gestures are no longer converted to scroll events. MouseTracker.pinchHandler should be used instead. (#369) +* BREAKING CHANGE: MouseTracker now uses pointer enter/leave DOM events instead of pointer over/out DOM events for generating enterHandler/exitHandler events and for tracking pointers inside the hit-test bounds of the tracked element. (#369) * DEPRECATION: overlay functions have been moved from Drawer to Viewer (#331) * DEPRECATION: OpenSeadragon.cancelFullScreen has been renamed OpenSeadragon.exitFullScreen (#358) -* DEPRECATION: The 'isTouchEvent' property passed in MouseTracker events is deprecated and has been replaced with 'pointerType', which is a String value "mouse", "touch", "pen", or "" to support multiple simultaneous pointing devices (#369) +* DEPRECATION: The 'isTouchEvent' property passed in MouseTracker events is deprecated and has been replaced with 'pointerType', which is a String value "mouse", "touch", "pen", etc. to support multiple simultaneous pointing devices (#369) +* DEPRECATION: The 'buttonDownAny' property passed in MouseTracker enter and exit events (enterHandler/exitHandler) is deprecated and has been replaced with 'buttons', which indicates the button(s) currently pressed (#369) +* DEPRECATION: The 'buttonDownAny' property passed in Viewer's 'container-enter' and 'container-exit' events is deprecated and has been replaced with 'buttons', which indicates the button(s) currently pressed (#369) * Added layers support. Multiple images can now been displayed on top of each other with transparency via the Viewer.addLayer method (#298) * Improved overlay functions (#331) * Fixed: Nav button highlight states aren't quite aligned on Firefox (#303) @@ -28,7 +32,7 @@ OPENSEADRAGON CHANGELOG * Added optimization for large numbers of overlays: `checkResize = false` option for OpenSeadragon.Overlay (#365) * Updated full screen API, adding support for Opera and IE11 and allowing keyboard input in Chrome (#358) * Enhanced MouseTracker for multi-touch (#369) - * Added support for multiple touch-points on multiple/simultaneous pointing devices + * Added support for tracking multiple touch-points on multiple/simultaneous pointing devices * Added support for the W3C Pointer Events event model. Enables touch/multi-touch on IE10+ * Added a dragEndHandler event callback, called when a drag gesture ends * Added a pinchHandler event callback, called as a pinch gesture (2 touch points) is occurring diff --git a/src/mousetracker.js b/src/mousetracker.js index d7e4cb1c..09a5a8f5 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -34,15 +34,13 @@ (function ( $ ) { - // is any button currently being pressed while pointer events occur - var IS_BUTTON_DOWN = false, // dictionary from hash to private properties - THIS = {}; + var THIS = {}; /** * @class MouseTracker - * @classdesc Provides simplified handling of common pointing device (mouse, touch, pen) and keyboard + * @classdesc Provides simplified handling of common pointer device (mouse, touch, pen, etc.) and keyboard * events on a specific element, like 'enter', 'exit', 'press', 'release', * 'scroll', 'click', and 'drag'. * @@ -148,7 +146,7 @@ /** * @private * @property {Boolean} tracking - * Are we currently tracking pointer events. + * Are we currently tracking pointer events for this element. * @property {Boolean} capturing * Are we curruently capturing mouse events (legacy mouse events only). */ @@ -165,15 +163,13 @@ DOMMouseScroll: function ( event ) { onMouseWheel( _this, event ); }, MozMousePixelScroll: function ( event ) { onMouseWheel( _this, event ); }, - mouseover: function ( event ) { onMouseOver( _this, event ); }, - mouseout: function ( event ) { onMouseOut( _this, event ); }, + mouseenter: function ( event ) { onMouseEnter( _this, event ); }, + mouseleave: function ( event ) { onMouseLeave( _this, event ); }, mousedown: function ( event ) { onMouseDown( _this, event ); }, mouseup: function ( event ) { onMouseUp( _this, event ); }, mouseupcaptured: function ( event ) { onMouseUpCaptured( _this, event ); }, - //mouseupcapturedie: function ( event ) { onMouseUpCapturedIE( _this, event ); }, mousemove: function ( event ) { onMouseMove( _this, event ); }, mousemovecaptured: function ( event ) { onMouseMoveCaptured( _this, event ); }, - //mousemovecapturedie: function ( event ) { onMouseMoveCapturedIE( _this, event ); }, touchenter: function ( event ) { onTouchEnter( _this, event ); }, touchleave: function ( event ) { onTouchLeave( _this, event ); }, @@ -185,10 +181,10 @@ gesturestart: function ( event ) { onGestureStart( _this, event ); }, gesturechange: function ( event ) { onGestureChange( _this, event ); }, - pointerover: function ( event ) { onPointerOver( _this, event ); }, - MSPointerOver: function ( event ) { onPointerOver( _this, event ); }, - pointerout: function ( event ) { onPointerOut( _this, event ); }, - MSPointerOut: function ( event ) { onPointerOut( _this, event ); }, + pointerenter: function ( event ) { onPointerEnter( _this, event ); }, + MSPointerEnter: function ( event ) { onPointerEnter( _this, event ); }, + pointerleave: function ( event ) { onPointerLeave( _this, event ); }, + MSPointerLeave: function ( event ) { onPointerLeave( _this, event ); }, pointerdown: function ( event ) { onPointerDown( _this, event ); }, MSPointerDown: function ( event ) { onPointerDown( _this, event ); }, pointerup: function ( event ) { onPointerUp( _this, event ); }, @@ -199,12 +195,16 @@ MSPointerCancel: function ( event ) { onPointerCancel( _this, event ); }, tracking: false, + + // Active pointers lists. Array of GesturePointList objects, one for each pointer device type. + // GesturePointList objects are added each time a pointer is tracked by a new pointer device type (see getActivePointersListByType()). + // Active pointers are any pointer being tracked for this element which are in the hit-test area + // of the element (for hover-capable devices) and/or have contact or a button press initiated in the element. + activePointersLists: [], + + // Legacy mouse event tracking capturing: false, - // Active Contact Points - mousePoints: new $.MouseTracker.GesturePointList(), - touchPoints: new $.MouseTracker.GesturePointList(), - penPoints: new $.MouseTracker.GesturePointList(), // Tracking for pinch gesture pinchGPoints: [], lastPinchDist: 0, @@ -252,6 +252,30 @@ return this; }, + /** + * Returns the {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} for the given pointer device type, + * creating and caching a new {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} if one doesn't already exist for the type. + * @function + * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc. + * @returns {OpenSeadragon.MouseTracker.GesturePointList} + */ + getActivePointersListByType: function ( type ) { + var delegate = THIS[ this.hash ], + i, + len = delegate.activePointersLists.length, + list; + + for ( i = 0; i < len; i++ ) { + if ( delegate.activePointersLists[ i ].type === type ) { + return delegate.activePointersLists[ i ]; + } + } + + list = new $.MouseTracker.GesturePointList( type ); + delegate.activePointersLists.push( list ); + return list; + }, + /** * Implement or assign implementation to these handlers during or after * calling the constructor. @@ -260,14 +284,17 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. + * @param {Number} event.buttons + * Current buttons pressed. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @param {Boolean} event.insideElementPressed * True if the left mouse button is currently being pressed and was * initiated inside the tracked element, otherwise false. * @param {Boolean} event.buttonDownAny - * Was the button down anywhere in the screen during the event. + * Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. * @param {Boolean} event.isTouchEvent * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent @@ -287,14 +314,17 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. + * @param {Number} event.buttons + * Current buttons pressed. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @param {Boolean} event.insideElementPressed * True if the left mouse button is currently being pressed and was * initiated inside the tracked element, otherwise false. * @param {Boolean} event.buttonDownAny - * Was the button down anywhere in the screen during the event. + * Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. * @param {Boolean} event.isTouchEvent * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent @@ -314,9 +344,12 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. + * @param {Number} event.buttons + * Current buttons pressed. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @param {Boolean} event.isTouchEvent * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent @@ -336,14 +369,17 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. + * @param {Number} event.buttons + * Current buttons pressed. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @param {Boolean} event.insideElementPressed * True if the left mouse button is currently being pressed and was * initiated inside the tracked element, otherwise false. * @param {Boolean} event.insideElementReleased - * True if the cursor still inside the tracked element when the button was released. + * True if the cursor inside the tracked element when the button was released. * @param {Boolean} event.isTouchEvent * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent @@ -363,9 +399,12 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. + * @param {Number} event.buttons + * Current buttons pressed. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @param {Boolean} event.isTouchEvent * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent @@ -385,7 +424,7 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. * @param {Number} event.scroll @@ -411,10 +450,10 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. - * @param {Number} event.quick + * @param {Boolean} event.quick * True only if the clickDistThreshold and clickDeltaThreshold are both passed. Useful for ignoring events. * @param {Boolean} event.shift * True if the shift key was pressed during this event. @@ -437,14 +476,17 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. + * @param {Number} event.buttons + * Current buttons pressed. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @param {OpenSeadragon.Point} event.delta * The x,y components of the difference between the current position and the last drag event position. Useful for ignoring or weighting the events. - * @param {Number} speed + * @param {Number} event.speed * Current computed speed, in pixels per second. - * @property {Number} direction + * @param {Number} event.direction * Current computed direction, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0. * @param {Boolean} event.shift * True if the shift key was pressed during this event. @@ -467,12 +509,12 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. - * @param {Number} speed + * @param {Number} event.speed * Speed at the end of a drag gesture, in pixels per second. - * @property {Number} direction + * @param {Number} event.direction * Direction at the end of a drag gesture, expressed as an angle counterclockwise relative to the positive X axis (-pi to pi, in radians). Only valid if speed > 0. * @param {Boolean} event.shift * True if the shift key was pressed during this event. @@ -495,7 +537,7 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {Array.} event.gesturePoints * Gesture points associated with the gesture. Velocity data can be found here. * @param {OpenSeadragon.Point} event.lastCenter @@ -525,9 +567,12 @@ * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. * @param {String} event.pointerType - * "mouse", "touch", "pen", or "". + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. + * @param {Number} event.buttons + * Current buttons pressed. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @param {Boolean} event.isTouchEvent * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent @@ -595,12 +640,12 @@ /** * 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 + * This is a singleton, used by all MouseTracker instances, as it is unlikely there will ever be more than * two active gesture pointers at a time. * + * @private * @member gesturePointVelocityTracker * @memberof OpenSeadragon.MouseTracker - * @private */ $.MouseTracker.gesturePointVelocityTracker = (function () { var trackerPoints = [], @@ -652,7 +697,7 @@ lastPos: gPoint.currentPos } ); - // Only fire up the interval timer when there's gesture points to track + // Only fire up the interval timer when there's gesture pointers to track if ( trackerPoints.length === 1 ) { lastTime = $.now(); intervalId = window.setInterval( _doTracking, 50 ); @@ -667,7 +712,7 @@ for ( i = 0; i < len; i++ ) { if ( trackerPoints[ i ].guid === guid ) { trackerPoints.splice( i, 1 ); - // Only run the interval timer if theres gesture points to track + // Only run the interval timer if theres gesture pointers to track len--; if ( len === 0 ) { window.clearInterval( intervalId ); @@ -685,7 +730,7 @@ /////////////////////////////////////////////////////////////////////////////// -// Event model detection +// Pointer event model and feature detection /////////////////////////////////////////////////////////////////////////////// /** @@ -708,39 +753,38 @@ if ( window.PointerEvent ) { // IE11 and other W3C Pointer Event implementations (see http://www.w3.org/TR/pointerevents) - $.MouseTracker.subscribeEvents.push( "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel" ); + $.MouseTracker.subscribeEvents.push( "pointerenter", "pointerleave", "pointerdown", "pointerup", "pointermove", "pointercancel" ); $.MouseTracker.unprefixedPointerEvents = true; if( navigator.maxTouchPoints ) { $.MouseTracker.maxTouchPoints = navigator.maxTouchPoints; - } - else { + } else { $.MouseTracker.maxTouchPoints = 0; } - } - else if ( window.MSPointerEvent ) { + $.MouseTracker.haveTouchEnter = true; + } else if ( window.MSPointerEvent ) { // IE10 - $.MouseTracker.subscribeEvents.push( "MSPointerOver", "MSPointerOut", "MSPointerDown", "MSPointerUp", "MSPointerMove", "MSPointerCancel" ); + $.MouseTracker.subscribeEvents.push( "MSPointerEnter", "MSPointerLeave", "MSPointerDown", "MSPointerUp", "MSPointerMove", "MSPointerCancel" ); $.MouseTracker.unprefixedPointerEvents = false; if( navigator.msMaxTouchPoints ) { $.MouseTracker.maxTouchPoints = navigator.msMaxTouchPoints; - } - else { + } else { $.MouseTracker.maxTouchPoints = 0; } - } - else { + $.MouseTracker.haveTouchEnter = true; + } else { // Legacy W3C mouse events - $.MouseTracker.subscribeEvents.push( "mouseover", "mouseout", "mousedown", "mouseup", "mousemove" ); + $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave", "mousedown", "mouseup", "mousemove" ); if ( 'ontouchstart' in window ) { // iOS, Android, and other W3c Touch Event implementations (see http://www.w3.org/TR/2011/WD-touch-events-20110505) $.MouseTracker.subscribeEvents.push( "touchstart", "touchend", "touchmove", "touchcancel" ); if ( 'ontouchenter' in window ) { $.MouseTracker.subscribeEvents.push( "touchenter", "touchleave" ); $.MouseTracker.haveTouchEnter = true; - } - else { + } else { $.MouseTracker.haveTouchEnter = false; } + } else { + $.MouseTracker.haveTouchEnter = false; } if ( 'ongesturestart' in window ) { // iOS (see https://developer.apple.com/library/safari/documentation/UserExperience/Reference/GestureEventClassReference/GestureEvent/GestureEvent.html) @@ -757,7 +801,7 @@ /////////////////////////////////////////////////////////////////////////////// /** - * Represents a point of contact on the screen made by a mouse cursor, pen, touch, or other pointing device. + * Represents a point of contact on the screen made by a mouse cursor, pen, touch, or other pointer device. * * @typedef {Object} GesturePoint * @memberof OpenSeadragon.MouseTracker @@ -765,7 +809,9 @@ * @property {Number} id * Identifier unique from all other active GesturePoints for a given pointer device. * @property {String} type - * The pointer device type: "mouse", "touch", "pen", or "". + * The pointer device type: "mouse", "touch", "pen", etc. + * @property {Boolean} captured + * True if events for the gesture point are captured to the tracked element. * @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 @@ -776,10 +822,10 @@ * Current computed speed, in pixels per second. * @property {Number} direction * 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 - * The initial pointer contact time, in milliseconds. + * @property {OpenSeadragon.Point} contactPos + * The initial pointer contact position, relative to the page including any scrolling. Only valid if the pointer has contact (pressed, touch contact, pen contact). + * @property {Number} contactTime + * The initial pointer contact time, in milliseconds. Only valid if the pointer has contact (pressed, touch contact, pen contact). * @property {OpenSeadragon.Point} lastPos * The last pointer position, relative to the page including any scrolling. * @property {Number} lastTime @@ -791,14 +837,36 @@ */ - /*** + /** * @class GesturePointList - * @classdesc Provides an abstraction for a set of {@link OpenSeadragon.MouseTracker.GesturePoint} objects. + * @classdesc Provides an abstraction for a set of active {@link OpenSeadragon.MouseTracker.GesturePoint|GesturePoint} objects for a given pointer device type. + * Active pointers are any pointer being tracked for this element which are in the hit-test area + * of the element (for hover-capable devices) and/or have contact or a button press initiated in the element. * @memberof OpenSeadragon.MouseTracker - * @private + * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc. */ - $.MouseTracker.GesturePointList = function () { + $.MouseTracker.GesturePointList = function ( type ) { this._gPoints = []; + /** + * The pointer device type: "mouse", "touch", "pen", etc. + * @member {String} type + * @memberof OpenSeadragon.MouseTracker.GesturePointList# + */ + this.type = type; + /** + * Current buttons pressed for the device. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. + * @member {Number} buttons + * @memberof OpenSeadragon.MouseTracker.GesturePointList# + */ + this.buttons = 0; + /** + * Current number of contact points (touch points, mouse down, etc.) for the device. + * Combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. + * @member {Number} contacts + * @memberof OpenSeadragon.MouseTracker.GesturePointList# + */ + this.contacts = 0; }; $.MouseTracker.GesturePointList.prototype = /** @lends OpenSeadragon.MouseTracker.GesturePointList.prototype */{ /** @@ -949,8 +1017,9 @@ if ( delegate.setCaptureCapable ) { // IE<10, Firefox, other browsers with setCapture()/releaseCapture() tracker.element.setCapture( true ); - } - else { + } else { + // For browsers without setCapture()/releaseCapture(), we emulate mouse capture by hanging listeners on the window object. + // (Note we listen on the capture phase so the captured handlers will get called first) $.addEvent( window, "mouseup", @@ -981,8 +1050,9 @@ if ( delegate.setCaptureCapable ) { // IE<10, Firefox, other browsers with setCapture()/releaseCapture() tracker.element.releaseCapture(); - } - else { + } else { + // For browsers without setCapture()/releaseCapture(), we emulate mouse capture by hanging listeners on the window object. + // (Note we listen on the capture phase so the captured handlers will get called first) $.removeEvent( window, "mousemove", @@ -1001,32 +1071,9 @@ } - /** - * @private - * @inner - */ - function getGPointsListByType( tracker, type ) { - var delegate = THIS[ tracker.hash ], - list; - if ( type === 'mouse' ) { - list = delegate.mousePoints; - } - else if ( type === 'touch' ) { - list = delegate.touchPoints; - } - else if ( type === 'pen' ) { - list = delegate.penPoints; - } - else { - list = null; - } - return list; - } - - /** * Gets a W3C Pointer Events model compatible pointer type string from a DOM pointer event. - * IE10 used a long integer value, but the W3C specification (and IE11+) use a string "mouse", "touch", "pen", or "". + * IE10 used a long integer value, but the W3C specification (and IE11+) use a string "mouse", "touch", "pen", etc. * @private * @inner */ @@ -1034,8 +1081,7 @@ var pointerTypeStr; if ( $.MouseTracker.unprefixedPointerEvents ) { pointerTypeStr = event.pointerType; - } - else { + } else { // IE10 // MSPOINTER_TYPE_TOUCH: 0x00000002 // MSPOINTER_TYPE_PEN: 0x00000003 @@ -1094,7 +1140,7 @@ /////////////////////////////////////////////////////////////////////////////// -// DOM event handlers +// Device-specific DOM event handlers /////////////////////////////////////////////////////////////////////////////// /** @@ -1228,526 +1274,6 @@ } - /** - * @private - * @inner - */ - function onMouseOver( tracker, event ) { - var gPoint; - - event = $.getEvent( event ); - - gPoint = { - id: $.MouseTracker.mousePointerId, - type: 'mouse', - isPrimary: true, - insideElement: true, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - updatePointersOver( tracker, event, [ gPoint ] ); - } - - - /** - * @private - * @inner - */ - function onMouseOut( tracker, event ) { - var gPoint; - - event = $.getEvent( event ); - - gPoint = { - id: $.MouseTracker.mousePointerId, - type: 'mouse', - isPrimary: true, - insideElement: false, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - updatePointersOut( tracker, event, [ gPoint ] ); - } - - - /** - * @private - * @inner - */ - function onMouseDown( tracker, event ) { - var gPoint; - - event = $.getEvent( event ); - - if ( event.button == 2 ) { - return; - } - - if ( tracker.pressHandler || tracker.clickHandler || tracker.dragHandler ) { - captureMouse( tracker ); - } - - gPoint = { - id: $.MouseTracker.mousePointerId, - type: 'mouse', - isPrimary: true, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - addPointers( tracker, event, [ gPoint ] ); - - if ( tracker.pressHandler || tracker.clickHandler || tracker.dragHandler ) { - $.cancelEvent( event ); - } - } - - - /** - * @private - * @inner - */ - function onMouseUp( tracker, event ) { - var delegate = THIS[ tracker.hash ]; - - if ( !delegate.capturing || delegate.setCaptureCapable ) { - handleMouseUp( tracker, event ); - } - - if ( delegate.capturing ) { - releaseMouse( tracker ); - } - } - - /** - * This handler is attached to the window object to emulate mouse capture. - * Only triggered in older W3C browsers that don't have setCapture/releaseCapture - * methods or don't support the new pointer events model. - * onMouseUp is still called on the tracked element, so avoid processing twice. - * @private - * @inner - */ - function onMouseUpCaptured( tracker, event ) { - var delegate = THIS[ tracker.hash ]; - - handleMouseUp( tracker, event ); - - if ( delegate.capturing ) { - releaseMouse( tracker ); - } - } - - - /** - * @private - * @inner - */ - function onMouseMove( tracker, event ) { - var delegate = THIS[ tracker.hash ]; - if ( !delegate.capturing || delegate.setCaptureCapable ) { - handleMouseMove( tracker, event ); - } - } - - - /** - * This handler is attached to the window object to emulate mouse capture. - * Only triggered in older W3C browsers that don't have setCapture/releaseCapture - * methods or don't support the new pointer events model. - * onMouseMove is still called on the tracked element, so avoid processing twice. - * @private - * @inner - */ - function onMouseMoveCaptured( tracker, event ) { - handleMouseMove( tracker, event ); - } - - - /** - * @private - * @inner - */ - function onTouchEnter( tracker, event ) { - var i, - touchCount = event.changedTouches.length, - gPoints = []; - - for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { - id: event.changedTouches[ i ].identifier, - type: 'touch', - insideElement: true, - currentPos: getMouseAbsolute( event.changedTouches[ i ] ), - currentTime: $.now() - } ); - } - - updatePointersOver( tracker, event, gPoints ); - } - - - /** - * @private - * @inner - */ - function onTouchLeave( tracker, event ) { - var i, - touchCount = event.changedTouches.length, - gPoints = []; - - for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { - id: event.changedTouches[ i ].identifier, - type: 'touch', - insideElement: false, - currentPos: getMouseAbsolute( event.changedTouches[ i ] ), - currentTime: $.now() - } ); - } - - updatePointersOut( tracker, event, gPoints ); - } - - - /** - * @private - * @inner - */ - function onTouchStart( tracker, event ) { - var delegate = THIS[ tracker.hash ], - time, - primaryPoint, - primaryId, - gPoint, - i, - touchCount = event.changedTouches.length, - gPoints = []; - - time = $.now(); - - if ( !$.MouseTracker.haveTouchEnter && touchCount > 0 && delegate.touchPoints.getLength() === 0 ) { - gPoint = { - id: event.changedTouches[ 0 ].identifier, - type: 'touch', - insideElement: true, - currentPos: getMouseAbsolute( event.changedTouches[ 0 ] ), - currentTime: time - }; - 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 - } ); - } - - addPointers( tracker, event, gPoints ); - - if ( tracker.pressHandler || tracker.dragHandler || tracker.pinchHandler ) { - $.stopEvent( event ); - $.cancelEvent( event ); - return false; - } - } - - - /** - * @private - * @inner - */ - function onTouchEnd( tracker, event ) { - var delegate = THIS[ tracker.hash ], - time, - primaryPoint, - gPoint, - i, - touchCount = event.changedTouches.length, - gPoints = []; - - time = $.now(); - - for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { - id: event.changedTouches[ i ].identifier, - type: 'touch', - currentPos: getMouseAbsolute( event.changedTouches[ i ] ), - currentTime: time - } ); - } - - 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, - type: 'touch', - insideElement: false, - currentPos: getMouseAbsolute( event.changedTouches[ 0 ] ), - currentTime: time - }; - updatePointersOut( tracker, event, [ gPoint ] ); - } - - if ( tracker.pressHandler || tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { - $.stopEvent( event ); - $.cancelEvent( event ); - return false; - } - } - - - /** - * @private - * @inner - */ - function onTouchMove( tracker, event ) { - var i, - touchCount = event.changedTouches.length, - gPoints = []; - - for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { - id: event.changedTouches[ i ].identifier, - type: 'touch', - currentPos: getMouseAbsolute( event.changedTouches[ i ] ), - currentTime: $.now() - } ); - } - - updatePointers( tracker, event, gPoints ); - - if ( tracker.pressHandler || tracker.dragHandler || tracker.pinchHandler ) { - $.stopEvent( event ); - $.cancelEvent( event ); - return false; - } - } - - - /** - * @private - * @inner - */ - function onTouchCancel( tracker, event ) { - var i, - touchCount = event.changedTouches.length, - gPoints = []; - - for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { - id: event.changedTouches[ i ].identifier, - type: 'touch', - } ); - } - - cancelPointers( tracker, event, gPoints ); - } - - - /** - * @private - * @inner - */ - function onGestureStart( tracker, event ) { - event.stopPropagation(); - event.preventDefault(); - return false; - } - - - /** - * @private - * @inner - */ - function onGestureChange( tracker, event ) { - event.stopPropagation(); - event.preventDefault(); - return false; - } - - - /** - * @private - * @inner - */ - function onPointerOver( tracker, event ) { - var gPoint; - - gPoint = { - id: event.pointerId, - type: getPointerType( event ), - isPrimary: event.isPrimary, - insideElement: true, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - updatePointersOver( tracker, event, [ gPoint ] ); - } - - - /** - * @private - * @inner - */ - function onPointerOut( tracker, event ) { - var gPoint; - - gPoint = { - id: event.pointerId, - type: getPointerType( event ), - isPrimary: event.isPrimary, - insideElement: false, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - updatePointersOut( tracker, event, [ gPoint ] ); - } - - - /** - * @private - * @inner - */ - function onPointerDown( tracker, event ) { - var gPoint; - - if ( event.button == 2 ) { - return; - } - - if ( $.MouseTracker.unprefixedPointerEvents ) { - event.currentTarget.setPointerCapture( event.pointerId ); - } - else { - event.currentTarget.msSetPointerCapture( event.pointerId ); - } - - gPoint = { - id: event.pointerId, - type: getPointerType( event ), - isPrimary: event.isPrimary, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - addPointers( tracker, event, [ gPoint ] ); - - if ( tracker.pressHandler || tracker.dragHandler || tracker.pinchHandler ) { - $.stopEvent( event ); - $.cancelEvent( event ); - return false; - } - } - - - /** - * @private - * @inner - */ - function onPointerUp( tracker, event ) { - var gPoint; - - if ( event.button == 2 ) { - return; - } - - if ( $.MouseTracker.unprefixedPointerEvents ) { - event.currentTarget.releasePointerCapture( event.pointerId ); - } - else { - event.currentTarget.msReleasePointerCapture( event.pointerId ); - } - - gPoint = { - id: event.pointerId, - type: getPointerType( event ), - isPrimary: event.isPrimary, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - removePointers(tracker, event, [ gPoint ]); - - if ( tracker.pressHandler || tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { - $.stopEvent( event ); - $.cancelEvent( event ); - return false; - } - } - - - /** - * @private - * @inner - */ - function onPointerMove( tracker, event ) { - // Pointer changed coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height) - var gPoint; - - gPoint = { - id: event.pointerId, - type: getPointerType( event ), - isPrimary: event.isPrimary, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - updatePointers(tracker, event, [ gPoint ]); - - if ( tracker.pressHandler || tracker.dragHandler || tracker.pinchHandler ) { - $.stopEvent( event ); - $.cancelEvent( event ); - return false; - } - } - - - /** - * @private - * @inner - */ - function onPointerCancel( tracker, event ) { - var gPoint; - - gPoint = { - id: event.pointerId, - type: getPointerType( event ), - isPrimary: event.isPrimary, - }; - - cancelPointers( tracker, event, [ gPoint ] ); - } - - -/////////////////////////////////////////////////////////////////////////////// -// DOM event handler utility functions -/////////////////////////////////////////////////////////////////////////////// - /** * Handles 'wheel' events. * The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()). @@ -1787,6 +1313,146 @@ } + /** + * @private + * @inner + */ + function onMouseEnter( tracker, event ) { + var gPoint; + + event = $.getEvent( event ); + + gPoint = { + id: $.MouseTracker.mousePointerId, + type: 'mouse', + isPrimary: true, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + updatePointersOver( tracker, event, [ gPoint ] ); + } + + + /** + * @private + * @inner + */ + function onMouseLeave( tracker, event ) { + var gPoint; + + event = $.getEvent( event ); + + gPoint = { + id: $.MouseTracker.mousePointerId, + type: 'mouse', + isPrimary: true, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + updatePointersOut( tracker, event, [ gPoint ] ); + } + + + /** + * @private + * @inner + */ + function onMouseDown( tracker, event ) { + var gPoint; + + event = $.getEvent( event ); + + gPoint = { + id: $.MouseTracker.mousePointerId, + type: 'mouse', + isPrimary: true, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + if ( updatePointersDown( tracker, event, [ gPoint ], event.button ) ) { + $.stopEvent( event ); + captureMouse( tracker ); + } + + if ( tracker.clickHandler || tracker.pressHandler || tracker.dragHandler || tracker.dragEndHandler ) { + $.cancelEvent( event ); + } + } + + + /** + * @private + * @inner + */ + function onMouseUp( tracker, event ) { + handleMouseUp( tracker, event ); + } + + /** + * This handler is attached to the window object (on the capture phase) to emulate mouse capture. + * Only triggered in W3C browsers that don't have setCapture/releaseCapture + * methods or don't support the new pointer events model. + * onMouseUp is still attached to the tracked element, so stop propagation to avoid processing twice. + * + * @private + * @inner + */ + function onMouseUpCaptured( tracker, event ) { + handleMouseUp( tracker, event ); + $.stopEvent( event ); + } + + + /** + * @private + * @inner + */ + function handleMouseUp( tracker, event ) { + var gPoint; + + event = $.getEvent( event ); + + gPoint = { + id: $.MouseTracker.mousePointerId, + type: 'mouse', + isPrimary: true, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + if ( updatePointersUp( tracker, event, [ gPoint ], event.button ) ) { + releaseMouse( tracker ); + } + } + + + /** + * @private + * @inner + */ + function onMouseMove( tracker, event ) { + handleMouseMove( tracker, event ); + } + + + /** + * This handler is attached to the window object (on the capture phase) to emulate mouse capture. + * Only triggered in W3C browsers that don't have setCapture/releaseCapture + * methods or don't support the new pointer events model. + * onMouseMove is still attached to the tracked element, so stop propagation to avoid processing twice. + * + * @private + * @inner + */ + function onMouseMoveCaptured( tracker, event ) { + handleMouseMove( tracker, event ); + $.stopEvent( event ); + } + + /** * @private * @inner @@ -1804,7 +1470,7 @@ currentTime: $.now() }; - updatePointers( tracker, event, [ gPoint ] ); + updatePointersMove( tracker, event, [ gPoint ] ); } @@ -1812,24 +1478,1023 @@ * @private * @inner */ - function handleMouseUp( tracker, event ) { - var gPoint; + function onTouchEnter( tracker, event ) { + var i, + touchCount = event.changedTouches.length, + gPoints = []; - event = $.getEvent( event ); - - if ( event.button == 2 ) { - return; + for ( i = 0; i < touchCount; i++ ) { + gPoints.push( { + id: event.changedTouches[ i ].identifier, + type: 'touch', + // isPrimary not set - let the updatePointers functions determine it + currentPos: getMouseAbsolute( event.changedTouches[ i ] ), + currentTime: $.now() + } ); } + updatePointersOver( tracker, event, gPoints ); + } + + + /** + * @private + * @inner + */ + function onTouchLeave( tracker, event ) { + var i, + touchCount = event.changedTouches.length, + gPoints = []; + + for ( i = 0; i < touchCount; i++ ) { + gPoints.push( { + id: event.changedTouches[ i ].identifier, + type: 'touch', + // isPrimary not set - let the updatePointers functions determine it + currentPos: getMouseAbsolute( event.changedTouches[ i ] ), + currentTime: $.now() + } ); + } + + updatePointersOut( tracker, event, gPoints ); + } + + + /** + * @private + * @inner + */ + function onTouchStart( tracker, event ) { + var time, + i, + touchCount = event.changedTouches.length, + gPoints = []; + + time = $.now(); + + for ( i = 0; i < touchCount; i++ ) { + gPoints.push( { + id: event.changedTouches[ i ].identifier, + type: 'touch', + // isPrimary not set - let the updatePointers functions determine it + currentPos: getMouseAbsolute( event.changedTouches[ i ] ), + currentTime: time + } ); + } + + // simulate touchenter if not natively available + if ( !$.MouseTracker.haveTouchEnter ) { + updatePointersOver( tracker, event, gPoints ); + } + + if ( updatePointersDown( tracker, event, gPoints, 0 ) ) { // 0 means primary button press/release or touch contact + // Touch event model start, end, and move events are always captured so we don't need to capture explicitly + } + + $.stopEvent( event ); + $.cancelEvent( event ); + } + + + /** + * @private + * @inner + */ + function onTouchEnd( tracker, event ) { + var time, + i, + touchCount = event.changedTouches.length, + gPoints = []; + + time = $.now(); + + for ( i = 0; i < touchCount; i++ ) { + gPoints.push( { + id: event.changedTouches[ i ].identifier, + type: 'touch', + // isPrimary not set - let the updatePointers functions determine it + currentPos: getMouseAbsolute( event.changedTouches[ i ] ), + currentTime: time + } ); + } + + // Touch event model start, end, and move events are always captured so we don't need to release capture. + // We'll ignore the should-release-capture return value here + updatePointersUp( tracker, event, gPoints, 0 ); // 0 means primary button press/release or touch contact + + // simulate touchleave if not natively available + if ( !$.MouseTracker.haveTouchEnter && touchCount > 0 ) { + updatePointersOut( tracker, event, gPoints ); + } + + $.stopEvent( event ); + $.cancelEvent( event ); + } + + + /** + * @private + * @inner + */ + function onTouchMove( tracker, event ) { + var i, + touchCount = event.changedTouches.length, + gPoints = []; + + for ( i = 0; i < touchCount; i++ ) { + gPoints.push( { + id: event.changedTouches[ i ].identifier, + type: 'touch', + // isPrimary not set - let the updatePointers functions determine it + currentPos: getMouseAbsolute( event.changedTouches[ i ] ), + currentTime: $.now() + } ); + } + + updatePointersMove( tracker, event, gPoints ); + + $.stopEvent( event ); + $.cancelEvent( event ); + } + + + /** + * @private + * @inner + */ + function onTouchCancel( tracker, event ) { + var i, + touchCount = event.changedTouches.length, + gPoints = []; + + for ( i = 0; i < touchCount; i++ ) { + gPoints.push( { + id: event.changedTouches[ i ].identifier, + type: 'touch', + } ); + } + + updatePointersCancel( tracker, event, gPoints ); + } + + + /** + * @private + * @inner + */ + function onGestureStart( tracker, event ) { + event.stopPropagation(); + event.preventDefault(); + return false; + } + + + /** + * @private + * @inner + */ + function onGestureChange( tracker, event ) { + event.stopPropagation(); + event.preventDefault(); + return false; + } + + + /** + * @private + * @inner + */ + function onPointerEnter( tracker, event ) { + var gPoint; + gPoint = { - id: $.MouseTracker.mousePointerId, - type: 'mouse', - isPrimary: true, + id: event.pointerId, + type: getPointerType( event ), + isPrimary: event.isPrimary, currentPos: getMouseAbsolute( event ), currentTime: $.now() }; - removePointers( tracker, event, [ gPoint ] ); + updatePointersOver( tracker, event, [ gPoint ] ); + } + + + /** + * @private + * @inner + */ + function onPointerLeave( tracker, event ) { + var gPoint; + + gPoint = { + id: event.pointerId, + type: getPointerType( event ), + isPrimary: event.isPrimary, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + updatePointersOut( tracker, event, [ gPoint ] ); + } + + + /** + * @private + * @inner + */ + function onPointerDown( tracker, event ) { + var gPoint; + + gPoint = { + id: event.pointerId, + type: getPointerType( event ), + isPrimary: event.isPrimary, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + if ( updatePointersDown( tracker, event, [ gPoint ], event.button ) ) { + if ( $.MouseTracker.unprefixedPointerEvents ) { + event.currentTarget.setPointerCapture( event.pointerId ); + } else { + event.currentTarget.msSetPointerCapture( event.pointerId ); + } + $.stopEvent( event ); + } + + if ( tracker.clickHandler || tracker.pressHandler || tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { + $.cancelEvent( event ); + } + } + + + /** + * @private + * @inner + */ + function onPointerUp( tracker, event ) { + var gPoint; + + gPoint = { + id: event.pointerId, + type: getPointerType( event ), + isPrimary: event.isPrimary, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + if ( updatePointersUp( tracker, event, [ gPoint ], event.button ) ) { + if ( $.MouseTracker.unprefixedPointerEvents ) { + event.currentTarget.releasePointerCapture( event.pointerId ); + } else { + event.currentTarget.msReleasePointerCapture( event.pointerId ); + } + } + } + + + /** + * @private + * @inner + */ + function onPointerMove( tracker, event ) { + // Pointer changed coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height) + var gPoint; + + gPoint = { + id: event.pointerId, + type: getPointerType( event ), + isPrimary: event.isPrimary, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + updatePointersMove( tracker, event, [ gPoint ] ); + } + + + /** + * @private + * @inner + */ + function onPointerCancel( tracker, event ) { + var gPoint; + + gPoint = { + id: event.pointerId, + type: getPointerType( event ), + }; + + updatePointersCancel( tracker, event, [ gPoint ] ); + } + + +/////////////////////////////////////////////////////////////////////////////// +// Device-agnostic DOM event handlers +/////////////////////////////////////////////////////////////////////////////// + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker.GesturePointList} pointsList + * The GesturePointList to track the pointer in. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint + * Gesture point to track. + * @returns {Number} Number of gesture points in pointsList. + */ + function startTrackingPointer( pointsList, gPoint ) { + + // If isPrimary is not known for the pointer then set it according to our rules: + // true if the first pointer in the gesture, otherwise false + if ( !gPoint.hasOwnProperty( 'isPrimary' ) ) { + if ( pointsList.getLength() === 0 ) { + gPoint.isPrimary = true; + } else { + gPoint.isPrimary = false; + } + } + gPoint.speed = 0; + gPoint.direction = 0; + gPoint.contactPos = gPoint.currentPos; + gPoint.contactTime = gPoint.currentTime; + gPoint.lastPos = gPoint.currentPos; + gPoint.lastTime = gPoint.currentTime; + + return pointsList.add( gPoint ); + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker.GesturePointList} pointsList + * The GesturePointList to stop tracking the pointer on. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint + * Gesture point to stop tracking. + * @returns {Number} Number of gesture points in pointsList. + */ + function stopTrackingPointer( pointsList, gPoint ) { + var listLength, + primaryPoint; + + if ( pointsList.getById( gPoint.id ) ) { + listLength = pointsList.removeById( gPoint.id ); + + // If isPrimary is not known for the pointer and we just removed the primary pointer from the list then we need to set another pointer as primary + if ( !gPoint.hasOwnProperty( 'isPrimary' ) ) { + primaryPoint = pointsList.getPrimary(); + if ( !primaryPoint ) { + primaryPoint = pointsList.getByIndex( 0 ); + if ( primaryPoint ) { + primaryPoint.isPrimary = true; + } + } + } + } else { + listLength = pointsList.getLength(); + } + + return listLength; + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {Object} event + * A reference to the originating DOM event. + * @param {Array.} gPoints + * Gesture points associated with the event. + */ + function updatePointersOver( tracker, event, gPoints ) { + var pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), + i, + gPointCount = gPoints.length, + curGPoint, + updateGPoint, + propagate; + + for ( i = 0; i < gPointCount; i++ ) { + curGPoint = gPoints[ i ]; + updateGPoint = pointsList.getById( curGPoint.id ); + + if ( updateGPoint ) { + // Already tracking the pointer...update it + updateGPoint.insideElement = true; + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = curGPoint.currentPos; + updateGPoint.currentTime = curGPoint.currentTime; + + curGPoint = updateGPoint; + } else { + // Initialize for tracking and add to the tracking list + curGPoint.captured = false; + curGPoint.insideElementPressed = false; + curGPoint.insideElement = true; + startTrackingPointer( pointsList, curGPoint ); + } + + // Enter + if ( tracker.enterHandler ) { + propagate = tracker.enterHandler( + { + eventSource: tracker, + pointerType: curGPoint.type, + position: getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), + buttons: pointsList.buttons, + insideElementPressed: curGPoint.insideElementPressed, + buttonDownAny: pointsList.buttons !== 0,// IS_BUTTON_DOWN, *deprecated* + isTouchEvent: curGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {Object} event + * A reference to the originating DOM event. + * @param {Array.} gPoints + * Gesture points associated with the event. + */ + function updatePointersOut( tracker, event, gPoints ) { + var delegate = THIS[ tracker.hash ], + pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), + i, + gPointCount = gPoints.length, + curGPoint, + updateGPoint, + propagate; + + for ( i = 0; i < gPointCount; i++ ) { + curGPoint = gPoints[ i ]; + updateGPoint = pointsList.getById( curGPoint.id ); + + if ( updateGPoint ) { + // Already tracking the pointer. If captured then update it, else stop tracking it + if ( updateGPoint.captured ) { + updateGPoint.insideElement = false; + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = curGPoint.currentPos; + updateGPoint.currentTime = curGPoint.currentTime; + } else { + stopTrackingPointer( pointsList, updateGPoint ); + } + + curGPoint = updateGPoint; + } + + // Exit + if ( tracker.exitHandler ) { + propagate = tracker.exitHandler( + { + eventSource: tracker, + pointerType: curGPoint.type, + position: getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), + buttons: pointsList.buttons, + insideElementPressed: updateGPoint ? updateGPoint.insideElementPressed : false, + buttonDownAny: pointsList.buttons !== 0,// IS_BUTTON_DOWN, *deprecated* + isTouchEvent: curGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {Object} event + * A reference to the originating DOM event. + * @param {Array.} gPoints + * Gesture points associated with the event. + * @param {Number} buttonChanged + * The button involved in the event: -1: none, 0: primary, 1: aux, 2: secondary, 3: X1, 4: X2, 5: pen eraser. + * Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model, + * only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events. + * + * @returns {Boolean} True if pointers should be captured to the tracked element, otherwise false. + */ + function updatePointersDown( tracker, event, gPoints, buttonChanged ) { + var delegate = THIS[ tracker.hash ], + propagate, + pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), + i, + gPointCount = gPoints.length, + curGPoint, + updateGPoint; + + if ( typeof event.buttons !== 'undefined' ) { + pointsList.buttons = event.buttons; + } else { + if ( buttonChanged === 0 ) { + // Primary + pointsList.buttons |= 1; + } else if ( buttonChanged === 1 ) { + // Aux + pointsList.buttons |= 4; + } else if ( buttonChanged === 2 ) { + // Secondary + pointsList.buttons |= 2; + } else if ( buttonChanged === 3 ) { + // X1 (Back) + pointsList.buttons |= 8; + } else if ( buttonChanged === 4 ) { + // X2 (Forward) + pointsList.buttons |= 16; + } else if ( buttonChanged === 5 ) { + // Pen Eraser + pointsList.buttons |= 32; + } + } + + // Only capture and track primary button, pen, and touch contacts + //if ( buttonChanged !== 0 ) { + if ( buttonChanged !== 0 && buttonChanged !== 1 ) { //TODO Remove this IE8 compatibility and use the line above + return false; + } + + for ( i = 0; i < gPointCount; i++ ) { + curGPoint = gPoints[ i ]; + updateGPoint = pointsList.getById( curGPoint.id ); + + if ( updateGPoint ) { + // Already tracking the pointer...update it + updateGPoint.captured = true; + updateGPoint.insideElementPressed = true; + updateGPoint.insideElement = true; + updateGPoint.contactPos = curGPoint.currentPos; + updateGPoint.contactTime = curGPoint.currentTime; + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = curGPoint.currentPos; + updateGPoint.currentTime = curGPoint.currentTime; + + curGPoint = updateGPoint; + } else { + // Initialize for tracking and add to the tracking list (no pointerover or pointermove event occurred before this) + curGPoint.captured = true; + curGPoint.insideElementPressed = true; + curGPoint.insideElement = true; + startTrackingPointer( pointsList, curGPoint ); + } + + pointsList.contacts++; + + if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { + $.MouseTracker.gesturePointVelocityTracker.addPoint( tracker, curGPoint ); + } + + if ( pointsList.contacts === 1 ) { + // Press + if ( tracker.pressHandler ) { + propagate = tracker.pressHandler( + { + eventSource: tracker, + pointerType: curGPoint.type, + position: getPointRelativeToAbsolute( curGPoint.contactPos, tracker.element ), + buttons: pointsList.buttons, + isTouchEvent: curGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } else if ( pointsList.contacts === 2 ) { + if ( tracker.pinchHandler && curGPoint.type === 'touch' ) { + // Initialize for pinch + delegate.pinchGPoints = pointsList.asArray(); + delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos ); + delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos ); + } + } + } + + return true; + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {Object} event + * A reference to the originating DOM event. + * @param {Array.} gPoints + * Gesture points associated with the event. + * @param {Number} buttonChanged + * The button involved in the event: -1: none, 0: primary, 1: aux, 2: secondary, 3: X1, 4: X2, 5: pen eraser. + * Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model, + * only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events. + * + * @returns {Boolean} True if pointer capture should be released from the tracked element, otherwise false. + */ + function updatePointersUp( tracker, event, gPoints, buttonChanged ) { + var delegate = THIS[ tracker.hash ], + pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), + propagate, + insideElementReleased, + releasePoint, + releaseTime, + i, + gPointCount = gPoints.length, + curGPoint, + updateGPoint, + releaseCapture = false, + wasCaptured = false; + + if ( typeof event.buttons !== 'undefined' ) { + pointsList.buttons = event.buttons; + } else { + if ( buttonChanged === 0 ) { + // Primary + pointsList.buttons ^= ~1; + } else if ( buttonChanged === 1 ) { + // Aux + pointsList.buttons ^= ~4; + } else if ( buttonChanged === 2 ) { + // Secondary + pointsList.buttons ^= ~2; + } else if ( buttonChanged === 3 ) { + // X1 (Back) + pointsList.buttons ^= ~8; + } else if ( buttonChanged === 4 ) { + // X2 (Forward) + pointsList.buttons ^= ~16; + } else if ( buttonChanged === 5 ) { + // Pen Eraser + pointsList.buttons ^= ~32; + } + } + + // Only capture and track primary button, pen, and touch contacts + //if ( buttonChanged !== 0 ) { + if ( buttonChanged !== 0 && buttonChanged !== 1 ) { //TODO Remove this IE8 compatibility and use the line above + return false; + } + + for ( i = 0; i < gPointCount; i++ ) { + curGPoint = gPoints[ i ]; + updateGPoint = pointsList.getById( curGPoint.id ); + + if ( updateGPoint ) { + // Update the pointer, stop tracking it if not still in this element + if ( updateGPoint.captured ) { + updateGPoint.captured = false; + releaseCapture = true; + wasCaptured = true; + } + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = curGPoint.currentPos; + updateGPoint.currentTime = curGPoint.currentTime; + if ( !updateGPoint.insideElement ) { + stopTrackingPointer( pointsList, updateGPoint ); + } + + releasePoint = updateGPoint.currentPos; + releaseTime = updateGPoint.currentTime; + + if ( wasCaptured ) { + // Pointer was activated in our element but could have been removed in any element since events are captured to our element + + pointsList.contacts--; + + if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { + $.MouseTracker.gesturePointVelocityTracker.removePoint( tracker, updateGPoint ); + } + + if ( pointsList.contacts === 0 ) { + + // Release (pressed in our element) + if ( tracker.releaseHandler ) { + propagate = tracker.releaseHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( releasePoint, tracker.element ), + buttons: pointsList.buttons, + insideElementPressed: updateGPoint.insideElementPressed, + insideElementReleased: updateGPoint.insideElement, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + + // Drag End + if ( tracker.dragEndHandler && !updateGPoint.currentPos.equals( updateGPoint.contactPos ) ) { + propagate = tracker.dragEndHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), + speed: updateGPoint.speed, + direction: updateGPoint.direction, + shift: event.shiftKey, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + + // Click + if ( tracker.clickHandler && updateGPoint.insideElementPressed && updateGPoint.insideElement ) { + var time = releaseTime - updateGPoint.contactTime, + distance = updateGPoint.contactPos.distanceTo( releasePoint ), + quick = time <= tracker.clickTimeThreshold && + distance <= tracker.clickDistThreshold; + + propagate = tracker.clickHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), + quick: quick, + shift: event.shiftKey, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } else if ( pointsList.contacts === 2 ) { + if ( tracker.pinchHandler && updateGPoint.type === 'touch' ) { + // Reset for pinch + delegate.pinchGPoints = pointsList.asArray(); + delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos ); + delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos ); + } + } + } else { + // Pointer was activated in another element but removed in our element + + // Release (pressed in another element) + if ( tracker.releaseHandler ) { + propagate = tracker.releaseHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( releasePoint, tracker.element ), + buttons: pointsList.buttons, + insideElementPressed: updateGPoint.insideElementPressed, + insideElementReleased: updateGPoint.insideElement, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + } + } + + return releaseCapture; + } + + + /** + * Call when pointer(s) change coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height) + * + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {Object} event + * A reference to the originating DOM event. + * @param {Array.} gPoints + * Gesture points associated with the event. + */ + function updatePointersMove( tracker, event, gPoints ) { + var delegate = THIS[ tracker.hash ], + pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), + i, + gPointCount = gPoints.length, + curGPoint, + updateGPoint, + gPointArray, + delta, + propagate; + + if ( typeof event.buttons !== 'undefined' ) { + pointsList.buttons = event.buttons; + } + + for ( i = 0; i < gPointCount; i++ ) { + curGPoint = gPoints[ i ]; + updateGPoint = pointsList.getById( curGPoint.id ); + + if ( updateGPoint ) { + // Already tracking the pointer...update it + if ( curGPoint.hasOwnProperty( 'isPrimary' ) ) { + updateGPoint.isPrimary = curGPoint.isPrimary; + } + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = curGPoint.currentPos; + updateGPoint.currentTime = curGPoint.currentTime; + } else { + // Initialize for tracking and add to the tracking list (no pointerover or pointerdown event occurred before this) + curGPoint.captured = false; + curGPoint.insideElementPressed = false; + curGPoint.insideElement = true; + startTrackingPointer( pointsList, curGPoint ); + } + } + + // Stop (mouse only) + if ( tracker.stopHandler && gPoints[ 0 ].type === 'mouse' ) { + clearTimeout( tracker.stopTimeOut ); + tracker.stopTimeOut = setTimeout( function() { + handlePointerStop( tracker, event, gPoints[ 0 ].type ); + }, tracker.stopDelay ); + } + + if ( pointsList.contacts === 0 ) { + // Move (no contacts: hovering mouse or other hover-capable device) + if ( tracker.moveHandler ) { + propagate = tracker.moveHandler( + { + eventSource: tracker, + pointerType: gPoints[ 0 ].type, + position: getPointRelativeToAbsolute( gPoints[ 0 ].currentPos, tracker.element ), + buttons: pointsList.buttons, + isTouchEvent: gPoints[ 0 ].type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } else if ( pointsList.contacts === 1 ) { + // Move (1 contact) + if ( tracker.moveHandler ) { + updateGPoint = pointsList.asArray()[ 0 ]; + propagate = tracker.moveHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), + buttons: pointsList.buttons, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + + // Drag + if ( tracker.dragHandler ) { + updateGPoint = pointsList.asArray()[ 0 ]; + delta = updateGPoint.currentPos.minus( updateGPoint.lastPos ); + propagate = tracker.dragHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), + buttons: pointsList.buttons, + delta: delta, + speed: updateGPoint.speed, + direction: updateGPoint.direction, + shift: event.shiftKey, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } else if ( pointsList.contacts === 2 ) { + // Move (2 contacts, use center) + if ( tracker.moveHandler ) { + gPointArray = pointsList.asArray(); + propagate = tracker.moveHandler( + { + eventSource: tracker, + pointerType: gPointArray[ 0 ].type, + position: getPointRelativeToAbsolute( getCenterPoint( gPointArray[ 0 ].currentPos, gPointArray[ 1 ].currentPos ), tracker.element ), + buttons: pointsList.buttons, + isTouchEvent: gPointArray[ 0 ].type === 'touch', + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + + // Pinch + if ( tracker.pinchHandler && gPoints[ 0 ].type === 'touch' ) { + delta = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos ); + if ( delta != delegate.currentPinchDist ) { + delegate.lastPinchDist = delegate.currentPinchDist; + delegate.currentPinchDist = delta; + delegate.lastPinchCenter = delegate.currentPinchCenter; + delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos ); + propagate = tracker.pinchHandler( + { + eventSource: tracker, + pointerType: 'touch', + gesturePoints: delegate.pinchGPoints, + lastCenter: getPointRelativeToAbsolute( delegate.lastPinchCenter, tracker.element ), + center: getPointRelativeToAbsolute( delegate.currentPinchCenter, tracker.element ), + lastDistance: delegate.lastPinchDist, + distance: delegate.currentPinchDist, + shift: event.shiftKey, + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( propagate === false ) { + $.cancelEvent( event ); + } + } + } + } + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {Object} event + * A reference to the originating DOM event. + * @param {Array.} gPoints + * Gesture points associated with the event. + */ + function updatePointersCancel( tracker, event, gPoints ) { + //removePointers( tracker, event, gPoints ); } @@ -1843,6 +2508,7 @@ eventSource: tracker, pointerType: pointerType, position: getMouseRelative( originalMoveEvent, tracker.element ), + buttons: tracker.getActivePointersListByType( pointerType ).buttons, isTouchEvent: pointerType === 'touch', originalEvent: originalMoveEvent, preventDefaultAction: false, @@ -1851,503 +2517,4 @@ } } - - /** - * @private - * @inner - */ - function addPointers( tracker, event, gPoints ) { - var delegate = THIS[ tracker.hash ], - propagate, - pointsList = getGPointsListByType( tracker, gPoints[ 0 ].type ), - pointsListLength, - i, - gPointCount = gPoints.length, - curGPoint; - - if ( pointsList ) { - for ( i = 0; i < gPointCount; i++ ) { - curGPoint = gPoints[ i ]; - - // Initialize for gesture tracking - curGPoint.insideElementPressed = true; - curGPoint.insideElement = true; - curGPoint.speed = 0; - curGPoint.direction = 0; - curGPoint.startPos = curGPoint.currentPos; - curGPoint.startTime = curGPoint.currentTime; - curGPoint.lastPos = curGPoint.currentPos; - curGPoint.lastTime = curGPoint.currentTime; - - if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { - $.MouseTracker.gesturePointVelocityTracker.addPoint( tracker, curGPoint ); - } - - pointsListLength = pointsList.add( curGPoint ); - - if ( pointsListLength == 1 ) { - // Press - if ( tracker.pressHandler ) { - propagate = tracker.pressHandler( - { - eventSource: tracker, - pointerType: curGPoint.type, - position: getPointRelativeToAbsolute( curGPoint.startPos, tracker.element ), - isTouchEvent: curGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - else if ( pointsListLength == 2 ) { - if ( tracker.pinchHandler && curGPoint.type === 'touch' ) { - // Initialize for pinch - delegate.pinchGPoints = pointsList.asArray(); - delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos ); - delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos ); - } - } - } - } - } - - - /** - * @private - * @inner - */ - function updatePointersOver( tracker, event, gPoints ) { - var delegate = THIS[ tracker.hash ], - pointsList = getGPointsListByType( tracker, gPoints[ 0 ].type ), - i, - gPointCount = gPoints.length, - curGPoint, - updateGPoint, - insideElementPressed, - propagate; - - if ( pointsList ) { - for ( i = 0; i < gPointCount; i++ ) { - curGPoint = gPoints[ i ]; - updateGPoint = pointsList.getById( curGPoint.id ); - - if ( updateGPoint ) { - updateGPoint.insideElement = true; - updateGPoint.lastPos = updateGPoint.currentPos; - updateGPoint.lastTime = updateGPoint.currentTime; - updateGPoint.currentPos = curGPoint.currentPos; - updateGPoint.currentTime = curGPoint.currentTime; - insideElementPressed = updateGPoint.insideElementPressed; - } - else { - insideElementPressed = false; - } - - // Enter - if ( tracker.enterHandler ) { - propagate = tracker.enterHandler( - { - eventSource: tracker, - pointerType: curGPoint.type, - position: getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), - insideElementPressed: insideElementPressed, - buttonDownAny: IS_BUTTON_DOWN, - isTouchEvent: curGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - } - } - - - /** - * @private - * @inner - */ - function updatePointersOut( tracker, event, gPoints ) { - var delegate = THIS[ tracker.hash ], - pointsList = getGPointsListByType( tracker, gPoints[ 0 ].type ), - i, - gPointCount = gPoints.length, - curGPoint, - updateGPoint, - insideElementPressed, - propagate; - - if ( pointsList ) { - for ( i = 0; i < gPointCount; i++ ) { - curGPoint = gPoints[ i ]; - updateGPoint = pointsList.getById( curGPoint.id ); - - if ( updateGPoint ) { - updateGPoint.insideElement = false; - updateGPoint.lastPos = updateGPoint.currentPos; - updateGPoint.lastTime = updateGPoint.currentTime; - updateGPoint.currentPos = curGPoint.currentPos; - updateGPoint.currentTime = curGPoint.currentTime; - insideElementPressed = updateGPoint.insideElementPressed; - } - else { - insideElementPressed = false; - } - - // Exit - if ( tracker.exitHandler ) { - propagate = tracker.exitHandler( - { - eventSource: tracker, - pointerType: curGPoint.type, - position: getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), - insideElementPressed: insideElementPressed, - buttonDownAny: IS_BUTTON_DOWN, - isTouchEvent: curGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - } - } - - - /** - * @private - * @inner - */ - function updatePointers( tracker, event, gPoints ) { - // Pointer(s) changed coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height) - var delegate = THIS[ tracker.hash ], - pointsList = getGPointsListByType( tracker, gPoints[ 0 ].type ), - pointsListLength, - i, - gPointCount = gPoints.length, - curGPoint, - updateGPoint, - gPointArray, - delta, - propagate; - - if ( pointsList ) { - pointsListLength = pointsList.getLength(); - - for ( i = 0; i < gPointCount; i++ ) { - curGPoint = gPoints[ i ]; - updateGPoint = pointsList.getById( curGPoint.id ); - - if ( updateGPoint ) { - updateGPoint.lastPos = updateGPoint.currentPos; - updateGPoint.lastTime = updateGPoint.currentTime; - updateGPoint.currentPos = curGPoint.currentPos; - updateGPoint.currentTime = curGPoint.currentTime; - } - } - - // 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, mouse or other hover-capable device) - if ( tracker.moveHandler ) { - propagate = tracker.moveHandler( - { - eventSource: tracker, - pointerType: gPoints[ 0 ].type, - position: getPointRelativeToAbsolute( gPoints[ 0 ].currentPos, tracker.element ), - isTouchEvent: gPoints[ 0 ].type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - else if ( pointsListLength === 1 ) { - // Move (1 contact) - if ( tracker.moveHandler ) { - updateGPoint = pointsList.asArray()[ 0 ]; - propagate = tracker.moveHandler( - { - eventSource: tracker, - pointerType: updateGPoint.type, - position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), - isTouchEvent: updateGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - - // Drag - if ( tracker.dragHandler ) { - updateGPoint = pointsList.asArray()[ 0 ]; - delta = updateGPoint.currentPos.minus( updateGPoint.lastPos ); - propagate = tracker.dragHandler( - { - eventSource: tracker, - pointerType: updateGPoint.type, - position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), - delta: delta, - speed: updateGPoint.speed, - direction: updateGPoint.direction, - shift: event.shiftKey, - isTouchEvent: updateGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - else if ( pointsListLength === 2 ) { - // Move (2 contacts, use center) - if ( tracker.moveHandler ) { - gPointArray = pointsList.asArray(); - propagate = tracker.moveHandler( - { - eventSource: tracker, - pointerType: gPointArray[ 0 ].type, - position: getPointRelativeToAbsolute( getCenterPoint( gPointArray[ 0 ].currentPos, gPointArray[ 1 ].currentPos ), tracker.element ), - isTouchEvent: gPointArray[ 0 ].type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - - // Pinch - if ( tracker.pinchHandler && gPoints[ 0 ].type === 'touch' ) { - delta = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos ); - if ( delta != delegate.currentPinchDist ) { - delegate.lastPinchDist = delegate.currentPinchDist; - delegate.currentPinchDist = delta; - delegate.lastPinchCenter = delegate.currentPinchCenter; - delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos ); - propagate = tracker.pinchHandler( - { - eventSource: tracker, - pointerType: 'touch', - gesturePoints: delegate.pinchGPoints, - lastCenter: getPointRelativeToAbsolute( delegate.lastPinchCenter, tracker.element ), - center: getPointRelativeToAbsolute( delegate.currentPinchCenter, tracker.element ), - lastDistance: delegate.lastPinchDist, - distance: delegate.currentPinchDist, - shift: event.shiftKey, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - } - } - } - - - /** - * @private - * @inner - */ - function removePointers( tracker, event, gPoints ) { - var delegate = THIS[ tracker.hash ], - pointsList = getGPointsListByType( tracker, gPoints[ 0 ].type ), - pointsListLength, - propagate, - insideElementPressed, - insideElementReleased, - releasePoint, - releaseTime, - i, - gPointCount = gPoints.length, - curGPoint, - updateGPoint, - removedGPoint; - - if ( pointsList ) { - for ( i = 0; i < gPointCount; i++ ) { - curGPoint = gPoints[ i ]; - - removedGPoint = pointsList.getById( curGPoint.id ); - - if ( removedGPoint ) { - - releasePoint = removedGPoint.currentPos; - releaseTime = removedGPoint.currentTime; - - if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { - $.MouseTracker.gesturePointVelocityTracker.removePoint( tracker, removedGPoint ); - } - - pointsListLength = pointsList.removeById( curGPoint.id ); - - if ( pointsListLength === 0 ) { - - insideElementPressed = removedGPoint.insideElementPressed; - insideElementReleased = removedGPoint.insideElement || $.pointInElement( tracker.element, releasePoint ); - - // Release - if ( tracker.releaseHandler ) { - propagate = tracker.releaseHandler( - { - eventSource: tracker, - pointerType: removedGPoint.type, - position: getPointRelativeToAbsolute( releasePoint, tracker.element ), - insideElementPressed: insideElementPressed, - insideElementReleased: insideElementReleased, - isTouchEvent: removedGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - - // Drag End - if ( tracker.dragEndHandler ) { - propagate = tracker.dragEndHandler( - { - eventSource: tracker, - pointerType: removedGPoint.type, - position: getPointRelativeToAbsolute( removedGPoint.currentPos, tracker.element ), - speed: removedGPoint.speed, - direction: removedGPoint.direction, - shift: event.shiftKey, - isTouchEvent: removedGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - - // Click - if ( tracker.clickHandler && insideElementPressed && insideElementReleased ) { - var time = releaseTime - removedGPoint.startTime, - distance = removedGPoint.startPos.distanceTo( releasePoint ), - quick = time <= tracker.clickTimeThreshold && - distance <= tracker.clickDistThreshold; - - propagate = tracker.clickHandler( - { - eventSource: tracker, - pointerType: curGPoint.type, - position: getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), - quick: quick, - shift: event.shiftKey, - isTouchEvent: curGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - //else if ( pointsListLength === 1 ) { - //} - else if ( pointsListLength === 2 ) { - if ( tracker.pinchHandler && curGPoint.type === 'touch' ) { - // Reset for pinch - delegate.pinchGPoints = pointsList.asArray(); - delegate.lastPinchDist = delegate.currentPinchDist = delegate.pinchGPoints[ 0 ].currentPos.distanceTo( delegate.pinchGPoints[ 1 ].currentPos ); - delegate.lastPinchCenter = delegate.currentPinchCenter = getCenterPoint( delegate.pinchGPoints[ 0 ].currentPos, delegate.pinchGPoints[ 1 ].currentPos ); - } - } - } - } - } - } - - - /** - * @private - * @inner - */ - function cancelPointers( tracker, event, gPoints ) { - removePointers( tracker, event, gPoints ); - } - - -/////////////////////////////////////////////////////////////////////////////// -// Deprecated -/////////////////////////////////////////////////////////////////////////////// - - // TODO Do we really need these anymore (used as buttonDownAny in enterHandler/exitHandler callbacks)? - // Surely there's a more robust and elegant solution... - - /** - * @private - * @inner - */ - function onGlobalMouseDown() { - IS_BUTTON_DOWN = true; - } - - /** - * @private - * @inner - */ - function onGlobalMouseUp() { - IS_BUTTON_DOWN = false; - } - - - (function () { - if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { - $.addEvent( document, "mousedown", onGlobalMouseDown, false ); - $.addEvent( document, "mouseup", onGlobalMouseUp, false ); - } else { - $.addEvent( window, "mousedown", onGlobalMouseDown, true ); - $.addEvent( window, "mouseup", onGlobalMouseUp, true ); - } - } )(); - } ( OpenSeadragon ) ); diff --git a/src/openseadragon.js b/src/openseadragon.js index de3ef7ee..e55cf3d7 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -285,24 +285,32 @@ * @property {Boolean} [gestureSettingsMouse.scrollToZoom=true] - Zoom on scroll gesture * @property {Boolean} [gestureSettingsMouse.clickToZoom=true] - Zoom on click gesture * @property {Boolean} [gestureSettingsMouse.flickEnabled=false] - Enable flick gesture - * @property {Number} [gestureSettingsMouse.flickMinSpeed=20] - Minimum speed to initiate a flick gesture (pixels-per-second) - * @property {Number} [gestureSettingsMouse.flickMomentum=0.35] - Momentum factor for the flick gesture + * @property {Number} [gestureSettingsMouse.flickMinSpeed=20] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second) + * @property {Number} [gestureSettingsMouse.flickMomentum=0.40] - If flickEnabled is true, the momentum factor for the flick gesture * * @property {OpenSeadragon.GestureSettings} [gestureSettingsTouch] * Settings for gestures generated by a touch pointer device. (See {@link OpenSeadragon.GestureSettings}) * @property {Boolean} [gestureSettingsTouch.scrollToZoom=false] - Zoom on scroll gesture * @property {Boolean} [gestureSettingsTouch.clickToZoom=false] - Zoom on click gesture * @property {Boolean} [gestureSettingsTouch.flickEnabled=true] - Enable flick gesture - * @property {Number} [gestureSettingsTouch.flickMinSpeed=20] - Minimum speed to initiate a flick gesture (pixels-per-second) - * @property {Number} [gestureSettingsTouch.flickMomentum=0.35] - Momentum factor for the flick gesture + * @property {Number} [gestureSettingsTouch.flickMinSpeed=20] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second) + * @property {Number} [gestureSettingsTouch.flickMomentum=0.40] - If flickEnabled is true, the momentum factor for the flick gesture * * @property {OpenSeadragon.GestureSettings} [gestureSettingsPen] * Settings for gestures generated by a pen pointer device. (See {@link OpenSeadragon.GestureSettings}) * @property {Boolean} [gestureSettingsPen.scrollToZoom=false] - Zoom on scroll gesture * @property {Boolean} [gestureSettingsPen.clickToZoom=true] - Zoom on click gesture * @property {Boolean} [gestureSettingsPen.flickEnabled=false] - Enable flick gesture - * @property {Number} [gestureSettingsPen.flickMinSpeed=20] - Minimum speed to initiate a flick gesture (pixels-per-second) - * @property {Number} [gestureSettingsPen.flickMomentum=0.35] - Momentum factor for the flick gesture + * @property {Number} [gestureSettingsPen.flickMinSpeed=20] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second) + * @property {Number} [gestureSettingsPen.flickMomentum=0.40] - If flickEnabled is true, the momentum factor for the flick gesture + * + * @property {OpenSeadragon.GestureSettings} [gestureSettingsUnknown] + * Settings for gestures generated by unknown pointer devices. (See {@link OpenSeadragon.GestureSettings}) + * @property {Boolean} [gestureSettingsUnknown.scrollToZoom=true] - Zoom on scroll gesture + * @property {Boolean} [gestureSettingsUnknown.clickToZoom=false] - Zoom on click gesture + * @property {Boolean} [gestureSettingsUnknown.flickEnabled=true] - Enable flick gesture + * @property {Number} [gestureSettingsUnknown.flickMinSpeed=20] - If flickEnabled is true, the minimum speed to initiate a flick gesture (pixels-per-second) + * @property {Number} [gestureSettingsUnknown.flickMomentum=0.40] - If flickEnabled is true, the momentum factor for the flick gesture * * @property {Number} [zoomPerClick=2.0] * The "zoom distance" per mouse click or touch tap. Note: Setting this to 1.0 effectively disables the click-to-zoom feature (also see gestureSettings[Mouse|Touch|Pen].clickToZoom). @@ -455,12 +463,12 @@ * Set to false to disable the kinetic panning effect (flick) at the end of a drag gesture. * * @property {Number} flickMinSpeed - * Minimum speed (in pixels-per-second) required to cause the kinetic panning effect (flick) at the end of a drag gesture. + * If flickEnabled is true, the minimum speed (in pixels-per-second) required to cause the kinetic panning effect (flick) at the end of a drag gesture. * * @property {Number} flickMomentum - * Constant multiplied by the velocity to determine the distance of the kinetic panning effect (flick) at the end of a drag gesture. + * If flickEnabled is true, a constant multiplied by the velocity to determine the distance of the kinetic panning effect (flick) at the end of a drag gesture. * A larger value will make the flick feel "lighter", while a smaller value will make the flick feel "heavier". - * Also, springStiffness and animationTime affect the "spring" used to stop the flick animation. + * Note: springStiffness and animationTime also affect the "spring" used to stop the flick animation. * */ @@ -798,6 +806,7 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ gestureSettingsMouse: { scrollToZoom: true, clickToZoom: true, flickEnabled: false, flickMinSpeed: 20, flickMomentum: 0.40 }, gestureSettingsTouch: { scrollToZoom: false, clickToZoom: false, flickEnabled: true, flickMinSpeed: 20, flickMomentum: 0.40 }, gestureSettingsPen: { scrollToZoom: false, clickToZoom: true, flickEnabled: false, flickMinSpeed: 20, flickMomentum: 0.40 }, + gestureSettingsUnknown: { scrollToZoom: false, clickToZoom: false, flickEnabled: true, flickMinSpeed: 20, flickMomentum: 0.40 }, zoomPerClick: 2, zoomPerScroll: 1.2, zoomPerSecond: 1.0, diff --git a/src/viewer.js b/src/viewer.js index 8c72347b..9285a501 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -278,8 +278,7 @@ $.Viewer = function( options ) { // Disable browser default touch handling if (style["touch-action"] !== undefined) { style["touch-action"] = "none"; - } - else if (style["-ms-touch-action"] !== undefined) { + } else if (style["-ms-touch-action"] !== undefined) { style["-ms-touch-action"] = "none"; } }(this.canvas.style)); @@ -1776,21 +1775,19 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, /** * Gets this viewer's gesture settings for the given pointer device type. * @method - * @param {String} type - The pointer device type to get the gesture settings for. "mouse", "touch", "pen", or "". + * @param {String} type - The pointer device type to get the gesture settings for ("mouse", "touch", "pen", etc.). * @return {OpenSeadragon.GestureSettings} */ gestureSettingsByDeviceType: function ( type ) { - if ( type === 'mouse' ) { - return this.gestureSettingsMouse; - } - else if ( type === 'touch' ) { - return this.gestureSettingsTouch; - } - else if ( type === 'pen' ) { - return this.gestureSettingsPen; - } - else { - return { scrollToZoom: false, clickToZoom: false, flickEnabled: false, flickMinSpeed: 20, flickMomentum: 0.35 }; + switch ( type ) { + case 'mouse': + return this.gestureSettingsMouse; + case 'touch': + return this.gestureSettingsTouch; + case 'pen': + return this.gestureSettingsPen; + default: + return this.gestureSettingsUnknown; } } @@ -2472,14 +2469,16 @@ function onContainerExit( event ) { * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. - * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. * @property {Object} originalEvent - The original DOM event. * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.raiseEvent( 'container-exit', { tracker: event.eventSource, position: event.position, + buttons: event.buttons, insideElementPressed: event.insideElementPressed, buttonDownAny: event.buttonDownAny, originalEvent: event.originalEvent @@ -2528,14 +2527,16 @@ function onContainerEnter( event ) { * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. - * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. * @property {Object} originalEvent - The original DOM event. * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.raiseEvent( 'container-enter', { tracker: event.eventSource, position: event.position, + buttons: event.buttons, insideElementPressed: event.insideElementPressed, buttonDownAny: event.buttonDownAny, originalEvent: event.originalEvent diff --git a/test/events.js b/test/events.js index 86f007c1..413dade3 100644 --- a/test/events.js +++ b/test/events.js @@ -25,184 +25,365 @@ } ); // ---------- - asyncTest( 'addHandler without userData', function () { - var openHandler = function ( event ) { - viewer.removeHandler( 'open', openHandler ); - ok( event, 'Event handler received event data' ); - if ( event ) { - strictEqual( event.eventSource, viewer, 'eventSource sent, eventSource is viewer' ); - strictEqual( event.userData, null, 'User data defaulted to null' ); - } - viewer.close(); - start(); - }; - - viewer.addHandler( 'open', openHandler ); - viewer.open( '/test/data/testpattern.dzi' ); - } ); - - // ---------- - asyncTest( 'addHandler with userData', function () { - var userData = { item1: 'Test user data', item2: Math.random() }, - originalUserData = { item1: userData.item1, item2: userData.item2 }; - - var openHandler = function ( event ) { - viewer.removeHandler( 'open', openHandler ); - ok( event, 'Event handler received event data' ); - ok( event && event.userData, 'Event handler received user data' ); - if ( event && event.userData ) { - deepEqual( event.userData, originalUserData, 'User data was untouched' ); - } - viewer.close(); - start(); - }; - - viewer.addHandler( 'open', openHandler, userData ); - viewer.open( '/test/data/testpattern.dzi' ); - } ); - - // ---------- - asyncTest( 'MouseTracker, EventSource canvas-drag canvas-release canvas-click', function () { + asyncTest( 'MouseTracker: mouse gestures', function () { var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ), - mouseTracker = null, - userData = { item1: 'Test user data', item2: Math.random() }, - originalUserData = { item1: userData.item1, item2: userData.item2 }, - dragCount = 10, - dragsHandledEventSource = 0, - releasesHandledEventSource = 0, - clicksHandledEventSource = 0, - eventsHandledMouseTracker = 0, - eventSourcePassedMouseTracker = 0, - originalEventsPassedMouseTracker = 0, - eventsHandledViewer = 0, - originalEventsPassedViewer = 0, - releasesExpected = 1, - clicksExpected = 1; + offset = $canvas.offset(), + tracker = viewer.innerTracker, + origEnterHandler, + origExitHandler, + origPressHandler, + origReleaseHandler, + origMoveHandler, + origClickHandler, + origDragHandler, + origDragEndHandler, + enterCount, + exitCount, + pressCount, + releaseCount, + moveCount, + clickCount, + dragCount, + dragEndCount, + insideElementPressed, + insideElementReleased, + quickClick; + + var hookViewerHandlers = function () { + origEnterHandler = tracker.enterHandler; + tracker.enterHandler = function ( event ) { + enterCount++; + if (origEnterHandler) { + return origEnterHandler( event ); + } else { + return true; + } + }; + origExitHandler = tracker.exitHandler; + tracker.exitHandler = function ( event ) { + exitCount++; + if (origExitHandler) { + return origExitHandler( event ); + } else { + return true; + } + }; + origPressHandler = tracker.pressHandler; + tracker.pressHandler = function ( event ) { + pressCount++; + if (origPressHandler) { + return origPressHandler( event ); + } else { + return true; + } + }; + origReleaseHandler = tracker.releaseHandler; + tracker.releaseHandler = function ( event ) { + releaseCount++; + insideElementPressed = event.insideElementPressed; + insideElementReleased = event.insideElementReleased; + if (origReleaseHandler) { + return origReleaseHandler( event ); + } else { + return true; + } + }; + origMoveHandler = tracker.moveHandler; + tracker.moveHandler = function ( event ) { + moveCount++; + if (origMoveHandler) { + return origMoveHandler( event ); + } else { + return true; + } + }; + origClickHandler = tracker.clickHandler; + tracker.clickHandler = function ( event ) { + clickCount++; + quickClick = event.quick; + if (origClickHandler) { + return origClickHandler( event ); + } else { + return true; + } + }; + origDragHandler = tracker.dragHandler; + tracker.dragHandler = function ( event ) { + dragCount++; + if (origDragHandler) { + return origDragHandler( event ); + } else { + return true; + } + }; + origDragEndHandler = tracker.dragEndHandler; + tracker.dragEndHandler = function ( event ) { + dragEndCount++; + if (origDragEndHandler) { + return origDragEndHandler( event ); + } else { + return true; + } + }; + }; + + var unhookViewerHandlers = function () { + tracker.enterHandler = origEnterHandler; + tracker.exitHandler = origExitHandler; + tracker.pressHandler = origPressHandler; + tracker.releaseHandler = origReleaseHandler; + tracker.moveHandler = origMoveHandler; + tracker.clickHandler = origClickHandler; + tracker.dragHandler = origDragHandler; + tracker.dragEndHandler = origDragEndHandler; + }; + + var simulateEnter = function ($element, event, x, y) { + event.clientX = offset.left + x; + event.clientY = offset.top + y; + $canvas.simulate( 'mouseenter', event ); + }; + + var simulateLeave = function ($element, event, x, y) { + event.clientX = offset.left + x; + event.clientY = offset.top + y; + $canvas.simulate( 'mouseleave', event ); + }; + + var simulateMove = function ($element, event, dX, dY, count) { + var i; + for ( i = 0; i < count; i++ ) { + event.clientX += dX; + event.clientY += dY; + $canvas.simulate( 'mousemove', event ); + } + }; + + var simulateDown = function ($element, event, x, y) { + event.clientX = offset.left + x; + event.clientY = offset.top + y; + $canvas.simulate( 'mousedown', event ); + }; + + var simulateUp = function ($element, event, x, y) { + event.clientX = offset.left + x; + event.clientY = offset.top + y; + $canvas.simulate( 'mouseup', event ); + }; + + var resetForAssessment = function () { + enterCount = 0; + exitCount = 0; + pressCount = 0; + releaseCount = 0; + moveCount = 0; + clickCount = 0; + dragCount = 0; + dragEndCount = 0; + insideElementPressed = false; + insideElementReleased = false; + quickClick = false; + }; + + var assessGestureExpectations = function (expected) { + var pointersList = tracker.getActivePointersListByType('mouse'); + if ('enterCount' in expected) { + equal( enterCount, expected.enterCount, expected.description + 'enterHandler event count matches expected (' + expected.enterCount + ')' ); + } + if ('exitCount' in expected) { + equal( exitCount, expected.exitCount, expected.description + 'exitHandler event count matches expected (' + expected.exitCount + ')' ); + } + if ('pressCount' in expected) { + equal( pressCount, expected.pressCount, expected.description + 'pressHandler event count matches expected (' + expected.pressCount + ')' ); + } + if ('releaseCount' in expected) { + equal( releaseCount, expected.releaseCount, expected.description + 'releaseHandler event count matches expected (' + expected.releaseCount + ')' ); + } + if ('moveCount' in expected) { + equal( moveCount, expected.moveCount, expected.description + 'moveHandler event count matches expected (' + expected.moveCount + ')' ); + } + if ('clickCount' in expected) { + equal( clickCount, expected.clickCount, expected.description + 'clickHandler event count matches expected (' + expected.clickCount + ')' ); + } + if ('dragCount' in expected) { + equal( dragCount, expected.dragCount, expected.description + 'dragHandler event count matches expected (' + expected.dragCount + ')' ); + } + if ('dragEndCount' in expected) { + equal( dragEndCount, expected.dragEndCount, expected.description + 'dragEndHandler event count matches expected (' + expected.dragEndCount + ')' ); + } + if ('insideElementPressed' in expected) { + equal( insideElementPressed, expected.insideElementPressed, expected.description + 'releaseHandler event.insideElementPressed matches expected (' + expected.insideElementPressed + ')' ); + } + if ('insideElementReleased' in expected) { + equal( insideElementReleased, expected.insideElementReleased, expected.description + 'releaseHandler event.insideElementReleased matches expected (' + expected.insideElementReleased + ')' ); + } + if ('contacts' in expected) { + equal( pointersList.contacts, expected.contacts, expected.description + 'Remaining pointer contact count matches expected (' + expected.contacts + ')' ); + } + if ('trackedPointers' in expected) { + equal( pointersList.getLength(), expected.trackedPointers, expected.description + 'Remaining tracked pointer count matches expected (' + expected.trackedPointers + ')' ); + } + if ('quickClick' in expected) { + equal( quickClick, expected.quickClick, expected.description + 'clickHandler event.quick matches expected (' + expected.quickClick + ')' ); + } + }; var onOpen = function ( event ) { viewer.removeHandler( 'open', onOpen ); - viewer.addHandler( 'canvas-drag', onEventSourceDrag ); - viewer.addHandler( 'canvas-release', onEventSourceRelease ); - viewer.addHandler( 'canvas-click', onEventSourceClick ); + hookViewerHandlers(); - mouseTracker = new OpenSeadragon.MouseTracker( { - element: $canvas[0], - userData: userData, - clickTimeThreshold: OpenSeadragon.DEFAULT_SETTINGS.clickTimeThreshold, - clickDistThreshold: OpenSeadragon.DEFAULT_SETTINGS.clickDistThreshold, - focusHandler: onMouseTrackerFocus, - blurHandler: onMouseTrackerBlur, - enterHandler: onMouseTrackerEnter, - pressHandler: onMouseTrackerPress, - moveHandler: onMouseTrackerMove, - dragHandler: onMouseTrackerDrag, - releaseHandler: onMouseTrackerRelease, - clickHandler: onMouseTrackerClick, - exitHandler: onMouseTrackerExit - } ).setTracking( true ); + // enter-move-release (release in tracked element, press in unknown element) + // (Note we also test to see if the pointer is still being tracked by not simulating a leave event until after assessment) + resetForAssessment(); + var event = {}; + simulateEnter($canvas, event, 0, 0); + simulateMove($canvas, event, 1, 1, 10); + simulateMove($canvas, event, -1, -1, 10); + simulateUp($canvas, event, 0, 0); + assessGestureExpectations({ + description: 'enter-move-release (release in tracked element, press in unknown element): ', + enterCount: 1, + exitCount: 0, + pressCount: 0, + releaseCount: 1, + moveCount: 20, + clickCount: 0, + dragCount: 0, + dragEndCount: 0, + insideElementPressed: false, + insideElementReleased: true, + contacts: 0, + trackedPointers: 1 + //quickClick: false + }); + simulateLeave($canvas, event, -1, -1); // flush tracked pointer - var event = { - clientX:1, - clientY:1 - }; + // enter-move-exit (fly-over) + resetForAssessment(); + var event = {}; + simulateEnter($canvas, event, 0, 0); + simulateMove($canvas, event, 1, 1, 10); + simulateMove($canvas, event, -1, -1, 10); + simulateLeave($canvas, event, -1, -1); + assessGestureExpectations({ + description: 'enter-move-exit (fly-over): ', + enterCount: 1, + exitCount: 1, + pressCount: 0, + releaseCount: 0, + moveCount: 20, + clickCount: 0, + dragCount: 0, + dragEndCount: 0, + //insideElementPressed: false, + //insideElementReleased: false, + contacts: 0, + trackedPointers: 0 + //quickClick: false + }); - $canvas.simulate( 'focus', event ); - Util.simulateViewerClickWithDrag( { - viewer: viewer, - widthFactor: 0.25, - heightFactor: 0.25, - dragCount: dragCount, - dragDx: 1, - dragDy: 1 - } ); - $canvas.simulate( 'blur', event ); - }; + // move-exit (fly-over, no enter event) + resetForAssessment(); + var event = {}; + simulateMove($canvas, event, 1, 1, 10); + simulateMove($canvas, event, -1, -1, 10); + simulateLeave($canvas, event, -1, -1); + assessGestureExpectations({ + description: 'move-exit (fly-over, no enter event): ', + enterCount: 0, + exitCount: 1, + pressCount: 0, + releaseCount: 0, + moveCount: 20, + clickCount: 0, + dragCount: 0, + dragEndCount: 0, + //insideElementPressed: false, + //insideElementReleased: false, + contacts: 0, + trackedPointers: 0 + //quickClick: false + }); - var checkOriginalEventReceivedViewer = function ( event ) { - eventsHandledViewer++; - //TODO Provide a better check for the original event...simulate doesn't currently extend the object - // with arbitrary user data. - if ( event && event.originalEvent ) { - originalEventsPassedViewer++; - } - }; + // enter-press-release-exit + resetForAssessment(); + var event = {}; + simulateEnter($canvas, event, 0, 0); + simulateDown($canvas, event, 0, 0); + simulateUp($canvas, event, 0, 0); + simulateLeave($canvas, event, -1, -1); + assessGestureExpectations({ + description: 'enter-press-release-exit (click): ', + enterCount: 1, + exitCount: 1, + pressCount: 1, + releaseCount: 1, + moveCount: 0, + clickCount: 1, + dragCount: 0, + dragEndCount: 0, + insideElementPressed: true, + insideElementReleased: true, + contacts: 0, + trackedPointers: 0, + quickClick: true + }); - var onEventSourceDrag = function ( event ) { - checkOriginalEventReceivedViewer( event ); - dragsHandledEventSource++; - }; + // enter-press-move-release-move-exit (drag, release in tracked element) + resetForAssessment(); + var event = {}; + simulateEnter($canvas, event, 0, 0); + simulateDown($canvas, event, 0, 0); + simulateMove($canvas, event, 1, 1, 10); + simulateUp($canvas, event, 10, 10); + simulateMove($canvas, event, -1, -1, 10); + simulateLeave($canvas, event, -1, -1); + assessGestureExpectations({ + description: 'enter-press-move-release-move-exit (drag, release in tracked element): ', + enterCount: 1, + exitCount: 1, + pressCount: 1, + releaseCount: 1, + moveCount: 20, + clickCount: 1, + dragCount: 10, + dragEndCount: 1, + insideElementPressed: true, + insideElementReleased: true, + contacts: 0, + trackedPointers: 0, + quickClick: false + }); - var onEventSourceRelease = function ( event ) { - checkOriginalEventReceivedViewer( event ); - releasesHandledEventSource++; - }; + // enter-press-move-exit-move-release (drag, release outside tracked element) + resetForAssessment(); + var event = {}; + simulateEnter($canvas, event, 0, 0); + simulateDown($canvas, event, 0, 0); + simulateMove($canvas, event, 1, 1, 5); + simulateMove($canvas, event, -1, -1, 5); + simulateLeave($canvas, event, -1, -1); + simulateMove($canvas, event, -1, -1, 5); + simulateUp($canvas, event, -5, -5); + assessGestureExpectations({ + description: 'enter-press-move-exit-move-release (drag, release outside tracked element): ', + enterCount: 1, + exitCount: 1, + pressCount: 1, + releaseCount: 1, + moveCount: 15, + clickCount: 0, + dragCount: 15, + dragEndCount: 1, + insideElementPressed: true, + insideElementReleased: false, + contacts: 0, + trackedPointers: 0, + quickClick: false + }); - var onEventSourceClick = function ( event ) { - checkOriginalEventReceivedViewer( event ); - clicksHandledEventSource++; - }; - - var checkOriginalEventReceived = function ( event ) { - eventsHandledMouseTracker++; - if ( event && event.eventSource === mouseTracker ) { - eventSourcePassedMouseTracker++; - } - //TODO Provide a better check for the original event...simulate doesn't currently extend the object - // with arbitrary user data. - if ( event && event.originalEvent ) { - originalEventsPassedMouseTracker++; - } - }; - - var onMouseTrackerFocus = function ( event ) { - checkOriginalEventReceived( event ); - }; - - var onMouseTrackerBlur = function ( event ) { - checkOriginalEventReceived( event ); - }; - - var onMouseTrackerEnter = function ( event ) { - checkOriginalEventReceived( event ); - }; - - var onMouseTrackerPress = function ( event ) { - checkOriginalEventReceived( event ); - }; - - var onMouseTrackerMove = function ( event ) { - checkOriginalEventReceived( event ); - }; - - var onMouseTrackerDrag = function ( event ) { - checkOriginalEventReceived( event ); - }; - - var onMouseTrackerRelease = function ( event ) { - checkOriginalEventReceived( event ); - }; - - var onMouseTrackerClick = function ( event ) { - checkOriginalEventReceived( event ); - }; - - var onMouseTrackerExit = function ( event ) { - checkOriginalEventReceived( event ); - - mouseTracker.destroy(); - viewer.removeHandler( 'canvas-drag', onEventSourceDrag ); - viewer.removeHandler( 'canvas-release', onEventSourceRelease ); - viewer.removeHandler( 'canvas-click', onEventSourceClick ); - - equal( dragsHandledEventSource, dragCount, "'canvas-drag' event count matches 'mousemove' event count (" + dragCount + ")" ); - equal( releasesHandledEventSource, releasesExpected, "'canvas-release' event count matches expected (" + releasesExpected + ")" ); - equal( clicksHandledEventSource, releasesExpected, "'canvas-click' event count matches expected (" + releasesExpected + ")" ); - equal( originalEventsPassedViewer, eventsHandledViewer, "Original event received count matches expected (" + eventsHandledViewer + ")" ); - - equal( eventSourcePassedMouseTracker, eventsHandledMouseTracker, "Event source received count matches expected (" + eventsHandledMouseTracker + ")" ); - equal( originalEventsPassedMouseTracker, eventsHandledMouseTracker, "Original event received count matches expected (" + eventsHandledMouseTracker + ")" ); - deepEqual( event.userData, originalUserData, 'MouseTracker userData was untouched' ); + unhookViewerHandlers(); viewer.close(); start(); @@ -213,7 +394,7 @@ } ); // ---------- - asyncTest( 'MouseTracker preventDefaultAction', function () { + asyncTest( 'MouseTracker: preventDefaultAction', function () { var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ), tracker = viewer.innerTracker, origClickHandler, @@ -281,7 +462,209 @@ } ); // ---------- - asyncTest( 'tile-drawing event', function () { + asyncTest( 'EventSource/MouseTracker/Viewer: event.originalEvent event.userData canvas-drag canvas-drag-end canvas-release canvas-click', function () { + var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ), + mouseTracker = null, + userData = { item1: 'Test user data', item2: Math.random() }, + originalUserData = { item1: userData.item1, item2: userData.item2 }, + dragCount = 10, + dragsHandledEventSource = 0, + dragEndsHandledEventSource = 0, + releasesHandledEventSource = 0, + clicksHandledEventSource = 0, + eventsHandledMouseTracker = 0, + eventSourcePassedMouseTracker = 0, + originalEventsPassedMouseTracker = 0, + eventsHandledViewer = 0, + originalEventsPassedViewer = 0, + dragEndsExpected = 1, + releasesExpected = 1, + clicksExpected = 1; + + var onOpen = function ( event ) { + viewer.removeHandler( 'open', onOpen ); + + viewer.addHandler( 'canvas-drag', onEventSourceDrag ); + viewer.addHandler( 'canvas-drag-end', onEventSourceDragEnd ); + viewer.addHandler( 'canvas-release', onEventSourceRelease ); + viewer.addHandler( 'canvas-click', onEventSourceClick ); + + mouseTracker = new OpenSeadragon.MouseTracker( { + element: $canvas[0], + userData: userData, + clickTimeThreshold: OpenSeadragon.DEFAULT_SETTINGS.clickTimeThreshold, + clickDistThreshold: OpenSeadragon.DEFAULT_SETTINGS.clickDistThreshold, + focusHandler: onMouseTrackerFocus, + blurHandler: onMouseTrackerBlur, + enterHandler: onMouseTrackerEnter, + pressHandler: onMouseTrackerPress, + moveHandler: onMouseTrackerMove, + dragHandler: onMouseTrackerDrag, + dragEndHandler: onMouseTrackerDragEnd, + releaseHandler: onMouseTrackerRelease, + clickHandler: onMouseTrackerClick, + exitHandler: onMouseTrackerExit + } ).setTracking( true ); + + var event = { + clientX:1, + clientY:1 + }; + + $canvas.simulate( 'focus', event ); + Util.simulateViewerClickWithDrag( { + viewer: viewer, + widthFactor: 0.25, + heightFactor: 0.25, + dragCount: dragCount, + dragDx: 1, + dragDy: 1 + } ); + $canvas.simulate( 'blur', event ); + }; + + var checkOriginalEventReceivedViewer = function ( event ) { + eventsHandledViewer++; + //TODO Provide a better check for the original event...simulate doesn't currently extend the object + // with arbitrary user data. + if ( event && event.originalEvent ) { + originalEventsPassedViewer++; + } + }; + + var onEventSourceDrag = function ( event ) { + checkOriginalEventReceivedViewer( event ); + dragsHandledEventSource++; + }; + + var onEventSourceDragEnd = function ( event ) { + checkOriginalEventReceivedViewer( event ); + dragEndsHandledEventSource++; + }; + + var onEventSourceRelease = function ( event ) { + checkOriginalEventReceivedViewer( event ); + releasesHandledEventSource++; + }; + + var onEventSourceClick = function ( event ) { + checkOriginalEventReceivedViewer( event ); + clicksHandledEventSource++; + }; + + var checkOriginalEventReceived = function ( event ) { + eventsHandledMouseTracker++; + if ( event && event.eventSource === mouseTracker ) { + eventSourcePassedMouseTracker++; + } + //TODO Provide a better check for the original event...simulate doesn't currently extend the object + // with arbitrary user data. + if ( event && event.originalEvent ) { + originalEventsPassedMouseTracker++; + } + }; + + var onMouseTrackerFocus = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerBlur = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerEnter = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerPress = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerMove = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerDrag = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerDragEnd = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerRelease = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerClick = function ( event ) { + checkOriginalEventReceived( event ); + }; + + var onMouseTrackerExit = function ( event ) { + checkOriginalEventReceived( event ); + + mouseTracker.destroy(); + viewer.removeHandler( 'canvas-drag', onEventSourceDrag ); + viewer.removeHandler( 'canvas-release', onEventSourceRelease ); + viewer.removeHandler( 'canvas-click', onEventSourceClick ); + + equal( dragsHandledEventSource, dragCount, "'canvas-drag' event count matches 'mousemove' event count (" + dragCount + ")" ); + equal( dragEndsHandledEventSource, dragEndsExpected, "'canvas-drag-end' event count matches expected (" + dragEndsExpected + ")" ); + equal( releasesHandledEventSource, releasesExpected, "'canvas-release' event count matches expected (" + releasesExpected + ")" ); + equal( clicksHandledEventSource, releasesExpected, "'canvas-click' event count matches expected (" + releasesExpected + ")" ); + equal( originalEventsPassedViewer, eventsHandledViewer, "Original event received count matches expected (" + eventsHandledViewer + ")" ); + + equal( eventSourcePassedMouseTracker, eventsHandledMouseTracker, "Event source received count matches expected (" + eventsHandledMouseTracker + ")" ); + equal( originalEventsPassedMouseTracker, eventsHandledMouseTracker, "Original event received count matches expected (" + eventsHandledMouseTracker + ")" ); + deepEqual( event.userData, originalUserData, 'MouseTracker userData was untouched' ); + + viewer.close(); + start(); + }; + + viewer.addHandler( 'open', onOpen ); + viewer.open( '/test/data/testpattern.dzi' ); + } ); + + // ---------- + asyncTest( 'Viewer: addHandler without userData', function () { + var openHandler = function ( event ) { + viewer.removeHandler( 'open', openHandler ); + ok( event, 'Event handler received event data' ); + if ( event ) { + strictEqual( event.eventSource, viewer, 'eventSource sent, eventSource is viewer' ); + strictEqual( event.userData, null, 'User data defaulted to null' ); + } + viewer.close(); + start(); + }; + + viewer.addHandler( 'open', openHandler ); + viewer.open( '/test/data/testpattern.dzi' ); + } ); + + // ---------- + asyncTest( 'Viewer: addHandler with userData', function () { + var userData = { item1: 'Test user data', item2: Math.random() }, + originalUserData = { item1: userData.item1, item2: userData.item2 }; + + var openHandler = function ( event ) { + viewer.removeHandler( 'open', openHandler ); + ok( event, 'Event handler received event data' ); + ok( event && event.userData, 'Event handler received user data' ); + if ( event && event.userData ) { + deepEqual( event.userData, originalUserData, 'User data was untouched' ); + } + viewer.close(); + start(); + }; + + viewer.addHandler( 'open', openHandler, userData ); + viewer.open( '/test/data/testpattern.dzi' ); + } ); + + // ---------- + asyncTest( 'Viewer: tile-drawing event', function () { var tileDrawing = function ( event ) { viewer.removeHandler( 'tile-drawing', tileDrawing ); ok( event, 'Event handler should be invoked' ); diff --git a/test/legacy.mouse.shim.js b/test/legacy.mouse.shim.js index 8a503759..0402ed6e 100644 --- a/test/legacy.mouse.shim.js +++ b/test/legacy.mouse.shim.js @@ -11,17 +11,18 @@ $.MouseTracker.subscribeEvents.push( "MozMousePixelScroll" ); } - $.MouseTracker.subscribeEvents.push( "mouseover", "mouseout", "mousedown", "mouseup", "mousemove" ); + $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave", "mousedown", "mouseup", "mousemove" ); if ( 'ontouchstart' in window ) { // iOS, Android, and other W3c Touch Event implementations (see http://www.w3.org/TR/2011/WD-touch-events-20110505) $.MouseTracker.subscribeEvents.push( "touchstart", "touchend", "touchmove", "touchcancel" ); if ( 'ontouchenter' in window ) { $.MouseTracker.subscribeEvents.push( "touchenter", "touchleave" ); $.MouseTracker.haveTouchEnter = true; - } - else { + } else { $.MouseTracker.haveTouchEnter = false; } + } else { + $.MouseTracker.haveTouchEnter = false; } if ( 'ongesturestart' in window ) { // iOS (see https://developer.apple.com/library/safari/documentation/UserExperience/Reference/GestureEventClassReference/GestureEvent/GestureEvent.html) diff --git a/test/navigator.js b/test/navigator.js index 95631a91..a073fee8 100644 --- a/test/navigator.js +++ b/test/navigator.js @@ -189,7 +189,7 @@ QUnit.config.autostart = false; clientY:offset.top + locationY }; $canvas - .simulate('mouseover', event) + .simulate('mouseenter', event) .simulate('mousedown', event) .simulate('mouseup', event); }; diff --git a/test/test.js b/test/test.js index 2272ea9d..cc640abd 100644 --- a/test/test.js +++ b/test/test.js @@ -37,7 +37,7 @@ }; $canvas - .simulate( 'mouseover', event ) + .simulate( 'mouseenter', event ) .simulate( 'mousedown', event ); for ( var i = 0; i < args.dragCount; i++ ) { event.clientX += args.dragDx; @@ -47,7 +47,7 @@ } $canvas .simulate( 'mouseup', event ) - .simulate( 'mouseout', event ); + .simulate( 'mouseleave', event ); }, initializeTestDOM: function () {