diff --git a/changelog.txt b/changelog.txt index 66730e0e..3206e6f7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -7,8 +7,12 @@ 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) * 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", 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) @@ -17,7 +21,7 @@ OPENSEADRAGON CHANGELOG * Added crossOriginPolicy drawer configuration to enable or disable CORS image requests (#364) * Disabled CORS by default (#377) * Added a ControlAnchor.ABSOLUTE enumeration. Enables absolute positioning of control elements in the viewer (#310) -* Added a 'navigator-scroll' event to Navigator. Fired when mousewheel/pinch events occur in the navigator (#310) +* Added a 'navigator-scroll' event to Navigator. Fired when mousewheel events occur in the navigator (#310) * Added a navigatorMaintainSizeRatio option. If set to true, the navigator minimap resizes when the viewer element is resized (#310) * Added 'ABSOLUTE' as a navigatorPosition option, along with corresponding navigatorTop, navigatorLeft options. Allows the navigator minimap to be placed anywhere in the viewer (#310) * Enhanced the navigatorTop, navigatorLeft, navigatorHeight, and navigatorWidth options to allow a number for pixel units or a string for other element units (%, em, etc.) (#310) @@ -30,6 +34,20 @@ OPENSEADRAGON CHANGELOG * Various fixes to bring OpenSeadragon into W3C compliance (#375) * Added separate flags for turning off each of the nav buttons (#376) * Added support for query parameters in DZI tileSource URL (#378) +* Enhanced MouseTracker for multi-touch (#369) + * 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 + * Added real-time velocity (speed and direction) tracking to drag operations. 'speed' and 'direction' values are passed in the dragHandler and dragEndHandler event data +* Enhanced Viewer for multi-touch (#369) + * Added pinch zoom with the new MouseTracker pinchHandler. The 'pan' and 'zoom' Viewer events can be used to detect changes resulting in pinch gestures + * Added a "canvas-pinch" event fired by the pinch event handler + * Added flick gesture with the new MouseTracker dragEndHandler + * Added a "canvas-drag-end" event fired by the drag-end event handler + * Added a GestureSettings class for per-device gesture options. Currently has settings to enable/disable zoom-on-scroll, zoom-on-pinch, zoom-on-click, and flick gesture settings. + * Added GestureSettings objects for mouse, touch, and pen devices to the Viewer options giving users the ability to customize gesture handling in the viewer + * Added velocity (speed and direction) properties to the "canvas-drag" event 1.0.0: diff --git a/src/mousetracker.js b/src/mousetracker.js index 86238d8c..4405e93e 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -34,20 +34,13 @@ (function ( $ ) { - // is any button currently being pressed while mouse events occur - var IS_BUTTON_DOWN = false, - // is any tracker currently capturing? - IS_CAPTURING = false, - // dictionary from hash to MouseTracker - ACTIVE = {}, - // list of trackers interested in capture - CAPTURING = [], // dictionary from hash to private properties - THIS = {}; + var THIS = {}; + /** * @class MouseTracker - * @classdesc Provides simplified handling of common mouse, touch, 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'. * @@ -58,33 +51,37 @@ * the original positional arguments 'element', 'clickTimeThreshold', * and 'clickDistThreshold' in that order. * @param {Element|String} options.element - * A reference to an element or an element id for which the mouse/touch/key + * A reference to an element or an element id for which the pointer/key * events will be monitored. * @param {Number} options.clickTimeThreshold - * The number of milliseconds within which multiple mouse clicks + * The number of milliseconds within which multiple pointer clicks * will be treated as a single event. * @param {Number} options.clickDistThreshold - * The distance between mouse click within multiple mouse clicks + * The distance between pointer click within multiple pointer clicks * will be treated as a single event. * @param {Number} [options.stopDelay=50] - * The number of milliseconds without mouse move before the mouse stop + * The number of milliseconds without pointer move before the stop * event is fired. * @param {OpenSeadragon.EventHandler} [options.enterHandler=null] - * An optional handler for mouse enter. + * An optional handler for pointer enter. * @param {OpenSeadragon.EventHandler} [options.exitHandler=null] - * An optional handler for mouse exit. + * An optional handler for pointer exit. * @param {OpenSeadragon.EventHandler} [options.pressHandler=null] - * An optional handler for mouse press. + * An optional handler for pointer press. * @param {OpenSeadragon.EventHandler} [options.releaseHandler=null] - * An optional handler for mouse release. + * An optional handler for pointer release. * @param {OpenSeadragon.EventHandler} [options.moveHandler=null] - * An optional handler for mouse move. + * An optional handler for pointer move. * @param {OpenSeadragon.EventHandler} [options.scrollHandler=null] - * An optional handler for mouse scroll. + * An optional handler for mouse wheel scroll. * @param {OpenSeadragon.EventHandler} [options.clickHandler=null] - * An optional handler for mouse click. + * An optional handler for pointer click. * @param {OpenSeadragon.EventHandler} [options.dragHandler=null] - * An optional handler for mouse drag. + * An optional handler for the drag gesture. + * @param {OpenSeadragon.EventHandler} [options.dragEndHandler=null] + * An optional handler for after a drag gesture. + * @param {OpenSeadragon.EventHandler} [options.pinchHandler=null] + * An optional handler for the pinch gesture. * @param {OpenSeadragon.EventHandler} [options.keyHandler=null] * An optional handler for keypress. * @param {OpenSeadragon.EventHandler} [options.focusHandler=null] @@ -108,19 +105,19 @@ this.hash = Math.random(); // An unique hash for this tracker. /** - * The element for which mouse/touch/key events are being monitored. + * The element for which pointer events are being monitored. * @member {Element} element * @memberof OpenSeadragon.MouseTracker# */ this.element = $.getElement( options.element ); /** - * The number of milliseconds within which mutliple mouse clicks will be treated as a single event. + * The number of milliseconds within which mutliple pointer clicks will be treated as a single event. * @member {Number} clickTimeThreshold * @memberof OpenSeadragon.MouseTracker# */ this.clickTimeThreshold = options.clickTimeThreshold; /** - * The distance between mouse click within multiple mouse clicks will be treated as a single event. + * The distance between pointer click within multiple pointer clicks will be treated as a single event. * @member {Number} clickDistThreshold * @memberof OpenSeadragon.MouseTracker# */ @@ -136,6 +133,8 @@ this.scrollHandler = options.scrollHandler || null; this.clickHandler = options.clickHandler || null; this.dragHandler = options.dragHandler || null; + this.dragEndHandler = options.dragEndHandler || null; + this.pinchHandler = options.pinchHandler || null; this.stopHandler = options.stopHandler || null; this.keyHandler = options.keyHandler || null; this.focusHandler = options.focusHandler || null; @@ -147,50 +146,73 @@ /** * @private * @property {Boolean} tracking - * Are we currently tracking mouse events. + * Are we currently tracking pointer events for this element. * @property {Boolean} capturing - * Are we curruently capturing mouse events. - * @property {Boolean} insideElementPressed - * True if the left mouse button is currently being pressed and was - * initiated inside the tracked element, otherwise false. - * @property {Boolean} insideElement - * Are we currently inside the screen area of the tracked element. - * @property {OpenSeadragon.Point} lastPoint - * Position of last mouse down/move - * @property {Number} lastMouseDownTime - * Time of last mouse down. - * @property {OpenSeadragon.Point} lastMouseDownPoint - * Position of last mouse down + * Are we curruently capturing mouse events (legacy mouse events only). */ THIS[ this.hash ] = { - mouseover: function ( event ) { onMouseOver( _this, event, false ); }, - mouseout: function ( event ) { onMouseOut( _this, event, false ); }, - mousedown: function ( event ) { onMouseDown( _this, event ); }, - mouseup: function ( event ) { onMouseUp( _this, event, false ); }, - mousemove: function ( event ) { onMouseMove( _this, event ); }, - click: function ( event ) { onMouseClick( _this, event ); }, + setCaptureCapable: !!this.element.setCapture && !!this.element.releaseCapture, + + click: function ( event ) { onClick( _this, event ); }, + keypress: function ( event ) { onKeyPress( _this, event ); }, + focus: function ( event ) { onFocus( _this, event ); }, + blur: function ( event ) { onBlur( _this, event ); }, + wheel: function ( event ) { onWheel( _this, event ); }, mousewheel: function ( event ) { onMouseWheel( _this, event ); }, DOMMouseScroll: function ( event ) { onMouseWheel( _this, event ); }, MozMousePixelScroll: function ( event ) { onMouseWheel( _this, event ); }, - mouseupie: function ( event ) { onMouseUpIE( _this, event ); }, - mousemovecapturedie: function ( event ) { onMouseMoveCapturedIE( _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 ); }, - mousemovecaptured: function ( event ) { onMouseMoveCaptured( _this, event, false ); }, + mousemove: function ( event ) { onMouseMove( _this, event ); }, + mousemovecaptured: function ( event ) { onMouseMoveCaptured( _this, event ); }, + + touchenter: function ( event ) { onTouchEnter( _this, event ); }, + touchleave: function ( event ) { onTouchLeave( _this, event ); }, touchstart: function ( event ) { onTouchStart( _this, event ); }, - touchmove: function ( event ) { onTouchMove( _this, event ); }, touchend: function ( event ) { onTouchEnd( _this, event ); }, - keypress: function ( event ) { onKeyPress( _this, event ); }, - focus: function ( event ) { onFocus( _this, event ); }, - blur: function ( event ) { onBlur( _this, event ); }, + touchmove: function ( event ) { onTouchMove( _this, event ); }, + touchcancel: function ( event ) { onTouchCancel( _this, event ); }, + + gesturestart: function ( event ) { onGestureStart( _this, event ); }, + gesturechange: function ( event ) { onGestureChange( _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 ); }, + MSPointerUp: function ( event ) { onPointerUp( _this, event ); }, + pointermove: function ( event ) { onPointerMove( _this, event ); }, + MSPointerMove: function ( event ) { onPointerMove( _this, event ); }, + pointercancel: function ( event ) { onPointerCancel( _this, event ); }, + 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, - insideElementPressed: false, - insideElement: false, - lastPoint: null, - lastMouseDownTime: null, - lastMouseDownPoint: null, - lastPinchDelta: 0 + + // Tracking for pinch gesture + pinchGPoints: [], + lastPinchDist: 0, + currentPinchDist: 0, + lastPinchCenter: null, + currentPinchCenter: null }; }; @@ -198,7 +220,7 @@ $.MouseTracker.prototype = /** @lends OpenSeadragon.MouseTracker.prototype */{ /** - * Clean up any events or objects created by the mouse tracker. + * Clean up any events or objects created by the tracker. * @function */ destroy: function () { @@ -232,6 +254,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. @@ -239,15 +285,20 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "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. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -264,15 +315,20 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "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. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -289,10 +345,15 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "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. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -309,15 +370,20 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "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. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -334,10 +400,15 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "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. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -354,6 +425,8 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. * @param {Number} event.scroll @@ -361,7 +434,7 @@ * @param {Boolean} event.shift * True if the shift key was pressed during this event. * @param {Boolean} event.isTouchEvent - * True if the original event is a touch event, otherwise false. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. Touch devices no longer generate scroll event. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -378,14 +451,16 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "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. * @param {Boolean} event.isTouchEvent - * True if the original event is a touch event, otherwise false. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -402,14 +477,23 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "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 start drag and end drag. Usefule for ignoring or weighting the events. + * 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} event.speed + * Current computed speed, in pixels per second. + * @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. * @param {Boolean} event.isTouchEvent - * True if the original event is a touch event, otherwise false. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -426,10 +510,73 @@ * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. + * @param {String} event.pointerType + * "mouse", "touch", "pen", etc. * @param {OpenSeadragon.Point} event.position * The position of the event relative to the tracked element. + * @param {Number} event.speed + * Speed at the end of a drag gesture, in pixels per second. + * @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. * @param {Boolean} event.isTouchEvent - * True if the original event is a touch event, otherwise false. + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. + * @param {Object} event.originalEvent + * The original event object. + * @param {Boolean} event.preventDefaultAction + * Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + dragEndHandler: function () { }, + + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @param {Object} event + * @param {OpenSeadragon.MouseTracker} event.eventSource + * A reference to the tracker instance. + * @param {String} event.pointerType + * "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 + * The previous center point of the two pinch contact points relative to the tracked element. + * @param {OpenSeadragon.Point} event.center + * The center point of the two pinch contact points relative to the tracked element. + * @param {Number} event.lastDistance + * The previous distance between the two pinch contact points in CSS pixels. + * @param {Number} event.distance + * The distance between the two pinch contact points in CSS pixels. + * @param {Boolean} event.shift + * True if the shift key was pressed during this event. + * @param {Object} event.originalEvent + * The original event object. + * @param {Boolean} event.preventDefaultAction + * Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + pinchHandler: function () { }, + + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @param {Object} event + * @param {OpenSeadragon.MouseTracker} event.eventSource + * A reference to the tracker instance. + * @param {String} event.pointerType + * "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 * The original event object. * @param {Boolean} event.preventDefaultAction @@ -492,8 +639,104 @@ blurHandler: function () { } }; + /** - * Detect available mouse wheel event. + * Provides continuous computation of velocity (speed and direction) of active pointers. + * 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 + */ + $.MouseTracker.gesturePointVelocityTracker = (function () { + var trackerPoints = [], + intervalId = 0, + lastTime = 0; + + // Generates a unique identifier for a tracked gesture point + var _generateGuid = function ( tracker, gPoint ) { + return tracker.hash.toString() + gPoint.type + gPoint.id.toString(); + }; + + // Interval timer callback. Computes velocity for all tracked gesture points. + var _doTracking = function () { + var i, + len = trackerPoints.length, + trackPoint, + gPoint, + now = $.now(), + elapsedTime, + distance, + speed; + + elapsedTime = now - lastTime; + lastTime = now; + + for ( i = 0; i < len; i++ ) { + trackPoint = trackerPoints[ i ]; + gPoint = trackPoint.gPoint; + // Math.atan2 gives us just what we need for a velocity vector, as we can simply + // use cos()/sin() to extract the x/y velocity components. + gPoint.direction = Math.atan2( gPoint.currentPos.y - trackPoint.lastPos.y, gPoint.currentPos.x - trackPoint.lastPos.x ); + // speed = distance / elapsed time + distance = trackPoint.lastPos.distanceTo( gPoint.currentPos ); + trackPoint.lastPos = gPoint.currentPos; + speed = 1000 * distance / ( elapsedTime + 1 ); + // Simple biased average, favors the most recent speed computation. Smooths out erratic gestures a bit. + gPoint.speed = 0.75 * speed + 0.25 * gPoint.speed; + } + }; + + // Public. Add a gesture point to be tracked + var addPoint = function ( tracker, gPoint ) { + var guid = _generateGuid( tracker, gPoint ); + + trackerPoints.push( + { + guid: guid, + gPoint: gPoint, + lastPos: gPoint.currentPos + } ); + + // Only fire up the interval timer when there's gesture pointers to track + if ( trackerPoints.length === 1 ) { + lastTime = $.now(); + intervalId = window.setInterval( _doTracking, 50 ); + } + }; + + // Public. Stop tracking a gesture point + var removePoint = function ( tracker, gPoint ) { + var guid = _generateGuid( tracker, gPoint ), + i, + len = trackerPoints.length; + for ( i = 0; i < len; i++ ) { + if ( trackerPoints[ i ].guid === guid ) { + trackerPoints.splice( i, 1 ); + // Only run the interval timer if theres gesture pointers to track + len--; + if ( len === 0 ) { + window.clearInterval( intervalId ); + } + break; + } + } + }; + + return { + addPoint: addPoint, + removePoint: removePoint + }; + } )(); + + +/////////////////////////////////////////////////////////////////////////////// +// Pointer event model and feature detection +/////////////////////////////////////////////////////////////////////////////// + + /** + * Detect available mouse wheel event name. */ $.MouseTracker.wheelEventName = ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version > 8 ) || ( 'onwheel' in document.createElement( 'div' ) ) ? 'wheel' : // Modern browsers support 'wheel' @@ -501,31 +744,240 @@ 'DOMMouseScroll'; // Assume old Firefox /** - * Starts tracking mouse events on this element. + * Detect browser pointer device event model(s) and build appropriate list of events to subscribe to. + */ + $.MouseTracker.subscribeEvents = [ "click", "keypress", "focus", "blur", $.MouseTracker.wheelEventName ]; + + if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) { + // Older Firefox + $.MouseTracker.subscribeEvents.push( "MozMousePixelScroll" ); + } + + if ( window.PointerEvent ) { + // IE11 and other W3C Pointer Event implementations (see http://www.w3.org/TR/pointerevents) + $.MouseTracker.subscribeEvents.push( "pointerenter", "pointerleave", "pointerdown", "pointerup", "pointermove", "pointercancel" ); + $.MouseTracker.unprefixedPointerEvents = true; + if( navigator.maxTouchPoints ) { + $.MouseTracker.maxTouchPoints = navigator.maxTouchPoints; + } else { + $.MouseTracker.maxTouchPoints = 0; + } + $.MouseTracker.haveTouchEnter = true; + $.MouseTracker.haveMouseEnter = true; + } else if ( window.MSPointerEvent ) { + // IE10 + $.MouseTracker.subscribeEvents.push( "MSPointerEnter", "MSPointerLeave", "MSPointerDown", "MSPointerUp", "MSPointerMove", "MSPointerCancel" ); + $.MouseTracker.unprefixedPointerEvents = false; + if( navigator.msMaxTouchPoints ) { + $.MouseTracker.maxTouchPoints = navigator.msMaxTouchPoints; + } else { + $.MouseTracker.maxTouchPoints = 0; + } + $.MouseTracker.haveTouchEnter = true; + $.MouseTracker.haveMouseEnter = true; + } else { + // Legacy W3C mouse events + $.MouseTracker.subscribeEvents.push( "mousedown", "mouseup", "mousemove" ); + if ( 'onmouseenter' in window ) { + $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave" ); + $.MouseTracker.haveMouseEnter = true; + } else { + $.MouseTracker.subscribeEvents.push( "mouseover", "mouseout" ); + $.MouseTracker.haveMouseEnter = false; + } + 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 { + $.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) + // Subscribe to these to prevent default gesture handling + $.MouseTracker.subscribeEvents.push( "gesturestart", "gesturechange" ); + } + $.MouseTracker.mousePointerId = "legacy-mouse"; + $.MouseTracker.maxTouchPoints = 10; + } + + +/////////////////////////////////////////////////////////////////////////////// +// Classes and typedefs +/////////////////////////////////////////////////////////////////////////////// + + /** + * 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 + * + * @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", 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 + * True if button pressed or contact point initiated inside the screen area of the tracked element. + * @property {Boolean} insideElement + * True if pointer or contact point is currently inside the bounds of the tracked element. + * @property {Number} speed + * 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} 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 + * The last pointer contact time, in milliseconds. + * @property {OpenSeadragon.Point} currentPos + * The current pointer position, relative to the page including any scrolling. + * @property {Number} currentTime + * The current pointer contact time, in milliseconds. + */ + + + /** + * @class GesturePointList + * @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 + * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc. + */ + $.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. + * @member {Number} contacts + * @memberof OpenSeadragon.MouseTracker.GesturePointList# + */ + this.contacts = 0; + }; + $.MouseTracker.GesturePointList.prototype = /** @lends OpenSeadragon.MouseTracker.GesturePointList.prototype */{ + /** + * @function + * @returns {Number} Number of gesture points in the list. + */ + getLength: function () { + return this._gPoints.length; + }, + /** + * @function + * @returns {Array.} The list of gesture points in the list as an array (read-only). + */ + asArray: function () { + return this._gPoints; + }, + /** + * @function + * @param {OpenSeadragon.MouseTracker.GesturePoint} gesturePoint - A gesture point to add to the list. + * @returns {Number} Number of gesture points in the list. + */ + add: function ( gp ) { + return this._gPoints.push( gp ); + }, + /** + * @function + * @param {Number} id - The id of the gesture point to remove from the list. + * @returns {Number} Number of gesture points in the list. + */ + removeById: function ( id ) { + var i, + len = this._gPoints.length; + for ( i = 0; i < len; i++ ) { + if ( this._gPoints[ i ].id === id ) { + this._gPoints.splice( i, 1 ); + break; + } + } + return this._gPoints.length; + }, + /** + * @function + * @param {Number} index - The index of the gesture point to retrieve from the list. + * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point at the given index, or null if not found. + */ + getByIndex: function ( index ) { + if ( index < this._gPoints.length) { + return this._gPoints[ index ]; + } + + return null; + }, + /** + * @function + * @param {Number} id - The id of the gesture point to retrieve from the list. + * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The gesture point with the given id, or null if not found. + */ + getById: function ( id ) { + var i, + len = this._gPoints.length; + for ( i = 0; i < len; i++ ) { + if ( this._gPoints[ i ].id === id ) { + return this._gPoints[ i ]; + } + } + return null; + }, + /** + * @function + * @returns {OpenSeadragon.MouseTracker.GesturePoint|null} The primary gesture point in the list, or null if not found. + */ + getPrimary: function ( id ) { + var i, + len = this._gPoints.length; + for ( i = 0; i < len; i++ ) { + if ( this._gPoints[ i ].isPrimary ) { + return this._gPoints[ i ]; + } + } + return null; + } + }; + + +/////////////////////////////////////////////////////////////////////////////// +// Utility functions +/////////////////////////////////////////////////////////////////////////////// + + /** + * Starts tracking pointer events on the tracked element. * @private * @inner */ function startTracking( tracker ) { - var events = [ - "mouseover", "mouseout", "mousedown", "mouseup", "mousemove", - "click", - $.MouseTracker.wheelEventName, - "touchstart", "touchmove", "touchend", - "keypress", - "focus", "blur" - ], - delegate = THIS[ tracker.hash ], + var delegate = THIS[ tracker.hash ], event, i; - // Add 'MozMousePixelScroll' event handler for older Firefox - if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) { - events.push( "MozMousePixelScroll" ); - } - if ( !delegate.tracking ) { - for ( i = 0; i < events.length; i++ ) { - event = events[ i ]; + for ( i = 0; i < $.MouseTracker.subscribeEvents.length; i++ ) { + event = $.MouseTracker.subscribeEvents[ i ]; $.addEvent( tracker.element, event, @@ -534,36 +986,22 @@ ); } delegate.tracking = true; - ACTIVE[ tracker.hash ] = tracker; } } /** - * Stops tracking mouse events on this element. + * Stops tracking pointer events on the tracked element. * @private * @inner */ function stopTracking( tracker ) { - var events = [ - "mouseover", "mouseout", "mousedown", "mouseup", "mousemove", - "click", - $.MouseTracker.wheelEventName, - "touchstart", "touchmove", "touchend", - "keypress", - "focus", "blur" - ], - delegate = THIS[ tracker.hash ], + var delegate = THIS[ tracker.hash ], event, i; - // Remove 'MozMousePixelScroll' event handler for older Firefox - if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) { - events.push( "MozMousePixelScroll" ); - } - if ( delegate.tracking ) { - for ( i = 0; i < events.length; i++ ) { - event = events[ i ]; + for ( i = 0; i < $.MouseTracker.subscribeEvents.length; i++ ) { + event = $.MouseTracker.subscribeEvents[ i ]; $.removeEvent( tracker.element, event, @@ -574,47 +1012,24 @@ releaseMouse( tracker ); delegate.tracking = false; - delete ACTIVE[ tracker.hash ]; } } /** - * @private - * @inner - */ - function hasMouse( tracker ) { - return THIS[ tracker.hash ].insideElement; - } - - /** - * Begin capturing mouse events on this element. + * Begin capturing mouse events to the tracked element (legacy mouse events only). * @private * @inner */ function captureMouse( tracker ) { var delegate = THIS[ tracker.hash ]; - if ( !delegate.capturing ) { - if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { - $.removeEvent( - tracker.element, - "mouseup", - delegate.mouseup, - false - ); - $.addEvent( - tracker.element, - "mouseup", - delegate.mouseupie, - true - ); - $.addEvent( - tracker.element, - "mousemove", - delegate.mousemovecapturedie, - true - ); + if ( !delegate.capturing ) { + if ( delegate.setCaptureCapable ) { + // IE<10, Firefox, other browsers with setCapture()/releaseCapture() + tracker.element.setCapture( true ); } 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", @@ -634,34 +1049,20 @@ /** - * Stop capturing mouse events on this element. + * Stop capturing mouse events to the tracked element (legacy mouse events only). * @private * @inner */ function releaseMouse( tracker ) { var delegate = THIS[ tracker.hash ]; - if ( delegate.capturing ) { - if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { - $.removeEvent( - tracker.element, - "mousemove", - delegate.mousemovecapturedie, - true - ); - $.removeEvent( - tracker.element, - "mouseup", - delegate.mouseupie, - true - ); - $.addEvent( - tracker.element, - "mouseup", - delegate.mouseup, - false - ); + if ( delegate.capturing ) { + if ( delegate.setCaptureCapable ) { + // IE<10, Firefox, other browsers with setCapture()/releaseCapture() + tracker.element.releaseCapture(); } 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", @@ -681,14 +1082,110 @@ /** + * 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", etc. * @private * @inner */ - function triggerOthers( tracker, handler, event, isTouch ) { - var otherHash; - for ( otherHash in ACTIVE ) { - if ( ACTIVE.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { - handler( ACTIVE[ otherHash ], event, isTouch ); + function getPointerType( event ) { + var pointerTypeStr; + if ( $.MouseTracker.unprefixedPointerEvents ) { + pointerTypeStr = event.pointerType; + } else { + // IE10 + // MSPOINTER_TYPE_TOUCH: 0x00000002 + // MSPOINTER_TYPE_PEN: 0x00000003 + // MSPOINTER_TYPE_MOUSE: 0x00000004 + switch( event.pointerType ) + { + case 0x00000002: + pointerTypeStr = 'touch'; + break; + case 0x00000003: + pointerTypeStr = 'pen'; + break; + case 0x00000004: + pointerTypeStr = 'mouse'; + break; + default: + pointerTypeStr = ''; + } + } + return pointerTypeStr; + } + + + /** + * @private + * @inner + */ + function getMouseAbsolute( event ) { + return $.getMousePosition( event ); + } + + /** + * @private + * @inner + */ + function getMouseRelative( event, element ) { + return getPointRelativeToAbsolute( getMouseAbsolute( event ), element ); + } + + /** + * @private + * @inner + */ + function getPointRelativeToAbsolute( point, element ) { + var offset = $.getElementOffset( element ); + return point.minus( offset ); + } + + /** + * @private + * @inner + */ + function getCenterPoint( point1, point2 ) { + return new $.Point( ( point1.x + point2.x ) / 2, ( point1.y + point2.y ) / 2 ); + } + + +/////////////////////////////////////////////////////////////////////////////// +// Device-specific DOM event handlers +/////////////////////////////////////////////////////////////////////////////// + + /** + * @private + * @inner + */ + function onClick( tracker, event ) { + if ( tracker.clickHandler ) { + $.cancelEvent( event ); + } + } + + + /** + * @private + * @inner + */ + function onKeyPress( tracker, event ) { + //console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey ); + var propagate; + if ( tracker.keyHandler ) { + event = $.getEvent( event ); + propagate = tracker.keyHandler( + { + eventSource: tracker, + position: getMouseRelative( event, tracker.element ), + keyCode: event.keyCode ? event.keyCode : event.charCode, + shift: event.shiftKey, + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( !propagate ) { + $.cancelEvent( event ); } } } @@ -702,6 +1199,7 @@ //console.log( "focus %s", event ); var propagate; if ( tracker.focusHandler ) { + event = $.getEvent( event ); propagate = tracker.focusHandler( { eventSource: tracker, @@ -725,6 +1223,7 @@ //console.log( "blur %s", event ); var propagate; if ( tracker.blurHandler ) { + event = $.getEvent( event ); propagate = tracker.blurHandler( { eventSource: tracker, @@ -740,437 +1239,6 @@ } - /** - * @private - * @inner - */ - function onKeyPress( tracker, event ) { - //console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey ); - var propagate; - if ( tracker.keyHandler ) { - propagate = tracker.keyHandler( - { - eventSource: tracker, - position: getMouseRelative( event, tracker.element ), - keyCode: event.keyCode ? event.keyCode : event.charCode, - shift: event.shiftKey, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( !propagate ) { - $.cancelEvent( event ); - } - } - } - - - /** - * @private - * @inner - */ - function onMouseOver( tracker, event, isTouch ) { - - var delegate = THIS[ tracker.hash ], - propagate; - - isTouch = isTouch || false; - - event = $.getEvent( event ); - - if ( !isTouch ) { - if ( $.Browser.vendor == $.BROWSERS.IE && - $.Browser.version < 9 && - delegate.capturing && - !isChild( event.srcElement, tracker.element ) ) { - - triggerOthers( tracker, onMouseOver, event, isTouch ); - } - - var to = event.target ? - event.target : - event.srcElement, - from = event.relatedTarget ? - event.relatedTarget : - event.fromElement; - - if ( !isChild( tracker.element, to ) || - isChild( tracker.element, from ) ) { - return; - } - } - - delegate.insideElement = true; - - if ( tracker.enterHandler ) { - propagate = tracker.enterHandler( - { - eventSource: tracker, - position: getMouseRelative( isTouch ? event.changedTouches[ 0 ] : event, tracker.element ), - insideElementPressed: delegate.insideElementPressed, - buttonDownAny: IS_BUTTON_DOWN, - isTouchEvent: isTouch, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - - - /** - * @private - * @inner - */ - function onMouseOut( tracker, event, isTouch ) { - var delegate = THIS[ tracker.hash ], - propagate; - - isTouch = isTouch || false; - - event = $.getEvent( event ); - - if ( !isTouch ) { - if ( $.Browser.vendor == $.BROWSERS.IE && - $.Browser.version < 9 && - delegate.capturing && - !isChild( event.srcElement, tracker.element ) ) { - - triggerOthers( tracker, onMouseOut, event, isTouch ); - - } - - var from = event.target ? - event.target : - event.srcElement, - to = event.relatedTarget ? - event.relatedTarget : - event.toElement; - - if ( !isChild( tracker.element, from ) || - isChild( tracker.element, to ) ) { - return; - } - } - - delegate.insideElement = false; - - if ( tracker.exitHandler ) { - propagate = tracker.exitHandler( - { - eventSource: tracker, - position: getMouseRelative( isTouch ? event.changedTouches[ 0 ] : event, tracker.element ), - insideElementPressed: delegate.insideElementPressed, - buttonDownAny: IS_BUTTON_DOWN, - isTouchEvent: isTouch, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - } - - - /** - * @private - * @inner - */ - function onMouseDown( tracker, event, noCapture, isTouch ) { - var delegate = THIS[ tracker.hash ], - propagate; - - isTouch = isTouch || false; - - event = $.getEvent(event); - - var eventOrTouchPoint = isTouch ? event.touches[ 0 ] : event; - - if ( event.button == 2 ) { - return; - } - - delegate.insideElementPressed = true; - - delegate.lastPoint = getMouseAbsolute( eventOrTouchPoint ); - delegate.lastMouseDownPoint = delegate.lastPoint; - delegate.lastMouseDownTime = $.now(); - - if ( tracker.pressHandler ) { - propagate = tracker.pressHandler( - { - eventSource: tracker, - position: getMouseRelative( eventOrTouchPoint, tracker.element ), - isTouchEvent: isTouch, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - - if ( tracker.pressHandler || tracker.dragHandler ) { - $.cancelEvent( event ); - } - - if ( noCapture ) { - return; - } - - if ( isTouch || - !( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) || - !IS_CAPTURING ) { - captureMouse( tracker ); - IS_CAPTURING = true; - // reset to empty & add us - CAPTURING = [ tracker ]; - } else if ( $.Browser.vendor == $.BROWSERS.IE && $.Browser.version < 9 ) { - // add us to the list - CAPTURING.push( tracker ); - } - } - - /** - * @private - * @inner - */ - function onTouchStart( tracker, event ) { - var touchA, - touchB; - - if ( event.touches.length == 1 && - event.targetTouches.length == 1 && - event.changedTouches.length == 1 ) { - - THIS[ tracker.hash ].lastTouch = event.touches[ 0 ]; - onMouseOver( tracker, event, true ); - // call with no capture as the onMouseMoveCaptured will - // be triggered by onTouchMove - onMouseDown( tracker, event, true, true ); - } - - if ( event.touches.length == 2 ) { - - touchA = getMouseAbsolute( event.touches[ 0 ] ); - touchB = getMouseAbsolute( event.touches[ 1 ] ); - THIS[ tracker.hash ].lastPinchDelta = - Math.abs( touchA.x - touchB.x ) + - Math.abs( touchA.y - touchB.y ); - THIS[ tracker.hash ].pinchMidpoint = new $.Point( - ( touchA.x + touchB.x ) / 2, - ( touchA.y + touchB.y ) / 2 - ); - //$.console.debug("pinch start : "+THIS[ tracker.hash ].lastPinchDelta); - } - - event.preventDefault(); - } - - - /** - * @private - * @inner - */ - function onMouseUp( tracker, event, isTouch ) { - var delegate = THIS[ tracker.hash ], - //were we inside the tracked element when we were pressed - insideElementPressed = delegate.insideElementPressed, - //are we still inside the tracked element when we released - insideElementReleased = delegate.insideElement, - propagate; - - isTouch = isTouch || false; - - event = $.getEvent(event); - - if ( event.button == 2 ) { - return; - } - - delegate.insideElementPressed = false; - - if ( tracker.releaseHandler ) { - propagate = tracker.releaseHandler( - { - eventSource: tracker, - position: getMouseRelative( isTouch ? event.changedTouches[ 0 ] : event, tracker.element ), - insideElementPressed: insideElementPressed, - insideElementReleased: insideElementReleased, - isTouchEvent: isTouch, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - - if ( insideElementPressed && insideElementReleased ) { - handleMouseClick( tracker, event, isTouch ); - } - } - - - /** - * @private - * @inner - */ - function onTouchEnd( tracker, event ) { - - if ( event.touches.length === 0 && - event.targetTouches.length === 0 && - event.changedTouches.length == 1 ) { - - THIS[ tracker.hash ].lastTouch = null; - - // call with no release, as the mouse events are - // not registered in onTouchStart - onMouseUpCaptured( tracker, event, true, true ); - onMouseOut( tracker, event, true ); - } - if ( event.touches.length + event.changedTouches.length == 2 ) { - THIS[ tracker.hash ].lastPinchDelta = null; - THIS[ tracker.hash ].pinchMidpoint = null; - //$.console.debug("pinch end"); - } - event.preventDefault(); - } - - - /** - * Only triggered once by the deepest element that initially received - * the mouse down event. We want to make sure THIS event doesn't bubble. - * Instead, we want to trigger the elements that initially received the - * mouse down event (including this one) only if the mouse is no longer - * inside them. Then, we want to release capture, and emulate a regular - * mouseup on the event that this event was meant for. - * @private - * @inner - */ - function onMouseUpIE( tracker, event ) { - var othertracker, - i; - - event = $.getEvent( event ); - - if ( event.button == 2 ) { - return; - } - - for ( i = 0; i < CAPTURING.length; i++ ) { - othertracker = CAPTURING[ i ]; - if ( !hasMouse( othertracker ) ) { - onMouseUp( othertracker, event, false ); - } - } - - releaseMouse( tracker ); - IS_CAPTURING = false; - event.srcElement.fireEvent( - "on" + event.type, - document.createEventObject( event ) - ); - - $.stopEvent( event ); - } - - - /** - * Only triggered in W3C browsers by elements within which the mouse was - * initially pressed, since they are now listening to the window for - * mouseup during the capture phase. We shouldn't handle the mouseup - * here if the mouse is still inside this element, since the regular - * mouseup handler will still fire. - * @private - * @inner - */ - function onMouseUpCaptured( tracker, event, noRelease, isTouch ) { - isTouch = isTouch || false; - - if ( !THIS[ tracker.hash ].insideElement || isTouch ) { - onMouseUp( tracker, event, isTouch ); - } - - if ( noRelease ) { - return; - } - - releaseMouse( tracker ); - } - - - /** - * @private - * @inner - */ - function onMouseMove( tracker, event ) { - if ( tracker.moveHandler ) { - event = $.getEvent( event ); - - var propagate = tracker.moveHandler( - { - eventSource: tracker, - position: getMouseRelative( event, tracker.element ), - isTouchEvent: false, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - if ( tracker.stopHandler ) { - clearTimeout( tracker.stopTimeOut ); - tracker.stopTimeOut = setTimeout( function() { - onMouseStop( tracker, event ); - }, tracker.stopDelay ); - } - } - - /** - * @private - * @inner - */ - function onMouseStop( tracker, originalMoveEvent ) { - if ( tracker.stopHandler ) { - tracker.stopHandler( { - eventSource: tracker, - position: getMouseRelative( originalMoveEvent, tracker.element ), - isTouchEvent: false, - originalEvent: originalMoveEvent, - preventDefaultAction: false, - userData: tracker.userData - } ); - } - } - - /** - * @private - * @inner - */ - function onMouseClick( tracker, event ) { - if ( tracker.clickHandler ) { - $.cancelEvent( event ); - } - } - - /** * Handler for 'wheel' events * @@ -1178,7 +1246,7 @@ * @inner */ function onWheel( tracker, event ) { - handleWheelEvent( tracker, event, event, false ); + handleWheelEvent( tracker, event, event ); } @@ -1189,8 +1257,7 @@ * @inner */ function onMouseWheel( tracker, event ) { - // For legacy IE, access the global (window) event object - event = event || window.event; + event = $.getEvent( event ); // Simulate a 'wheel' event var simulatedEvent = { @@ -1213,23 +1280,21 @@ simulatedEvent.deltaY = event.detail; } - handleWheelEvent( tracker, simulatedEvent, event, false ); + handleWheelEvent( tracker, simulatedEvent, event ); } /** * Handles 'wheel' events. - * The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()) or onTouchMove(). + * The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()). * * @private * @inner */ - function handleWheelEvent( tracker, event, originalEvent, isTouch ) { + function handleWheelEvent( tracker, event, originalEvent ) { var nDelta = 0, propagate; - isTouch = isTouch || false; - // The nDelta variable is gated to provide smooth z-index scrolling // since the mouse wheel allows for substantial deltas meant for rapid // y-index scrolling. @@ -1241,10 +1306,11 @@ propagate = tracker.scrollHandler( { eventSource: tracker, + pointerType: 'mouse', position: getMouseRelative( event, tracker.element ), scroll: nDelta, shift: event.shiftKey, - isTouchEvent: isTouch, + isTouchEvent: false, originalEvent: originalEvent, preventDefaultAction: false, userData: tracker.userData @@ -1261,42 +1327,134 @@ * @private * @inner */ - function handleMouseClick( tracker, event, isTouch ) { - var delegate = THIS[ tracker.hash ], - propagate; + function isParentChild( parent, child ) + { + if ( parent === child ) { + return false; + } + while ( child && child !== parent ) { + child = child.parentNode; + } + return child === parent; + } - isTouch = isTouch || false; + + /** + * @private + * @inner + */ + function onMouseOver( tracker, event ) { + var gPoint; event = $.getEvent( event ); - var eventOrTouchPoint = isTouch ? event.changedTouches[ 0 ] : event; - - if ( event.button == 2 ) { + if ( this === event.relatedTarget || isParentChild( this, event.relatedTarget ) ) { return; } - var time = $.now() - delegate.lastMouseDownTime, - point = getMouseAbsolute( eventOrTouchPoint ), - distance = delegate.lastMouseDownPoint.distanceTo( point ), - quick = time <= tracker.clickTimeThreshold && - distance <= tracker.clickDistThreshold; + gPoint = { + id: $.MouseTracker.mousePointerId, + type: 'mouse', + isPrimary: true, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; - if ( tracker.clickHandler ) { - propagate = tracker.clickHandler( - { - eventSource: tracker, - position: getMouseRelative( eventOrTouchPoint, tracker.element ), - quick: quick, - shift: event.shiftKey, - isTouchEvent: isTouch, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } + updatePointersEnter( tracker, event, [ gPoint ] ); + } + + + /** + * @private + * @inner + */ + function onMouseOut( tracker, event ) { + var gPoint; + + event = $.getEvent( event ); + + if ( this === event.relatedTarget || isParentChild( this, event.relatedTarget ) ) { + return; + } + + gPoint = { + id: $.MouseTracker.mousePointerId, + type: 'mouse', + isPrimary: true, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + updatePointersExit( tracker, event, [ gPoint ] ); + } + + + /** + * @private + * @inner + */ + function onMouseEnter( tracker, event ) { + var gPoint; + + event = $.getEvent( event ); + + gPoint = { + id: $.MouseTracker.mousePointerId, + type: 'mouse', + isPrimary: true, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + updatePointersEnter( 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() + }; + + updatePointersExit( 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 ); } } @@ -1305,175 +1463,1135 @@ * @private * @inner */ - function onMouseMoveCaptured( tracker, event, isTouch ) { - var delegate = THIS[ tracker.hash ], - delta, - propagate, - point; + function onMouseUp( tracker, event ) { + handleMouseUp( tracker, event ); + } - isTouch = isTouch || false; + /** + * 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 ); + } - event = $.getEvent(event); - var eventOrTouchPoint = isTouch ? event.touches[ 0 ] : event; - point = getMouseAbsolute( eventOrTouchPoint ); - delta = point.minus( delegate.lastPoint ); - delegate.lastPoint = point; + /** + * @private + * @inner + */ + function handleMouseUp( tracker, event ) { + var gPoint; - if ( tracker.dragHandler ) { - propagate = tracker.dragHandler( - { - eventSource: tracker, - position: getMouseRelative( eventOrTouchPoint, tracker.element ), - delta: delta, - shift: event.shiftKey, - isTouchEvent: isTouch, - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } + 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 + */ + function handleMouseMove( tracker, event ) { + var gPoint; + + event = $.getEvent( event ); + + gPoint = { + id: $.MouseTracker.mousePointerId, + type: 'mouse', + isPrimary: true, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + updatePointersMove( tracker, event, [ gPoint ] ); + } + + + /** + * @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', + // isPrimary not set - let the updatePointers functions determine it + currentPos: getMouseAbsolute( event.changedTouches[ i ] ), + currentTime: $.now() + } ); + } + + updatePointersEnter( 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() + } ); + } + + updatePointersExit( 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 ) { + updatePointersEnter( 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 ) { + updatePointersExit( tracker, event, gPoints ); + } + + $.stopEvent( event ); + $.cancelEvent( event ); + } + + /** * @private * @inner */ function onTouchMove( tracker, event ) { - var touchA, - touchB, - pinchDelta; + var i, + touchCount = event.changedTouches.length, + gPoints = []; - if ( !THIS[ tracker.hash ].lastTouch ) { - 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() + } ); } - if ( event.touches.length === 1 && - event.targetTouches.length === 1 && - event.changedTouches.length === 1 && - THIS[ tracker.hash ].lastTouch.identifier === event.touches[ 0 ].identifier ) { - - onMouseMoveCaptured( tracker, event, true ); - - } else if ( event.touches.length === 2 ) { - - touchA = getMouseAbsolute( event.touches[ 0 ] ); - touchB = getMouseAbsolute( event.touches[ 1 ] ); - pinchDelta = - Math.abs( touchA.x - touchB.x ) + - Math.abs( touchA.y - touchB.y ); - - //TODO: make the 75px pinch threshold configurable - if ( Math.abs( THIS[ tracker.hash ].lastPinchDelta - pinchDelta ) > 75 ) { - //$.console.debug( "pinch delta : " + pinchDelta + " | previous : " + THIS[ tracker.hash ].lastPinchDelta); - - // Simulate a 'wheel' event - var simulatedEvent = { - target: event.target || event.srcElement, - type: "wheel", - shiftKey: event.shiftKey || false, - clientX: THIS[ tracker.hash ].pinchMidpoint.x, - clientY: THIS[ tracker.hash ].pinchMidpoint.y, - pageX: THIS[ tracker.hash ].pinchMidpoint.x, - pageY: THIS[ tracker.hash ].pinchMidpoint.y, - deltaMode: 1, // 0=pixel, 1=line, 2=page - deltaX: 0, - deltaY: ( THIS[ tracker.hash ].lastPinchDelta > pinchDelta ) ? 1 : -1, - deltaZ: 0 - }; - - handleWheelEvent( tracker, simulatedEvent, event, true ); - - THIS[ tracker.hash ].lastPinchDelta = pinchDelta; - } - } - event.preventDefault(); - } - - /** - * Only triggered once by the deepest element that initially received - * the mouse down event. Since no other element has captured the mouse, - * we want to trigger the elements that initially received the mouse - * down event (including this one). The the param tracker isn't used - * but for consistency with the other event handlers we include it. - * @private - * @inner - */ - function onMouseMoveCapturedIE( tracker, event ) { - var i; - for ( i = 0; i < CAPTURING.length; i++ ) { - onMouseMoveCaptured( CAPTURING[ i ], event, false ); - } + updatePointersMove( tracker, event, gPoints ); $.stopEvent( event ); + $.cancelEvent( event ); } + /** * @private * @inner */ - function getMouseAbsolute( event ) { - return $.getMousePosition( event ); + 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 getMouseRelative( event, element ) { - var mouse = $.getMousePosition( event ), - offset = $.getElementOffset( element ); - - return mouse.minus( offset ); + function onGestureStart( tracker, event ) { + event.stopPropagation(); + event.preventDefault(); + return false; } + /** * @private * @inner - * Returns true if elementB is a child node of elementA, or if they're equal. */ - function isChild( elementA, elementB ) { - var body = document.body; - while ( elementB && elementA != elementB && body != elementB ) { - try { - elementB = elementB.parentNode; - } catch ( e ) { - return false; + function onGestureChange( tracker, event ) { + event.stopPropagation(); + event.preventDefault(); + return false; + } + + + /** + * @private + * @inner + */ + function onPointerEnter( tracker, event ) { + var gPoint; + + gPoint = { + id: event.pointerId, + type: getPointerType( event ), + isPrimary: event.isPrimary, + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + updatePointersEnter( 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() + }; + + updatePointersExit( 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 ); } } - return elementA == elementB; } + /** * @private * @inner */ - function onGlobalMouseDown() { - IS_BUTTON_DOWN = true; + 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 onGlobalMouseUp() { - IS_BUTTON_DOWN = false; + function onPointerCancel( tracker, event ) { + var gPoint; + + gPoint = { + id: event.pointerId, + type: getPointerType( event ), + }; + + updatePointersCancel( tracker, event, [ gPoint ] ); } - (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 ); +/////////////////////////////////////////////////////////////////////////////// +// 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 updatePointersEnter( 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, + 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 updatePointersExit( 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, + 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 commented 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 commented 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 ) { + updatePointersUp( tracker, event, gPoints, 0 ); + updatePointersExit( tracker, event, gPoints ); + } + + + /** + * @private + * @inner + */ + function handlePointerStop( tracker, originalMoveEvent, pointerType ) { + if ( tracker.stopHandler ) { + tracker.stopHandler( { + eventSource: tracker, + pointerType: pointerType, + position: getMouseRelative( originalMoveEvent, tracker.element ), + buttons: tracker.getActivePointersListByType( pointerType ).buttons, + isTouchEvent: pointerType === 'touch', + originalEvent: originalMoveEvent, + preventDefaultAction: false, + userData: tracker.userData + } ); + } + } } ( OpenSeadragon ) ); diff --git a/src/openseadragon.js b/src/openseadragon.js index 3be339af..c7f69749 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -185,10 +185,6 @@ * * @property {String} [debugGridColor='#437AB2'] * - * @property {Number} [animationTime=1.2] - * Specifies the animation duration per each {@link OpenSeadragon.Spring} - * which occur when the image is dragged or zoomed. - * * @property {Number} [blendTime=0] * Specifies the duration of animation as higher or lower level tiles are * replacing the existing tile. @@ -265,8 +261,6 @@ * achieved. Setting this to 0 and wrapHorizontal ( or wrapVertical ) to * true will provide the effect of an infinitely scrolling viewport. * - * @property {Number} [springStiffness=7.0] - * * @property {Number} [imageLoaderLimit=0] * The maximum number of image requests to make concurrently. By default * it is set to 0 allowing the browser to make the maximum number of @@ -280,11 +274,53 @@ * If a mouse or touch drag occurs and the distance to the starting drag * point is less than this many pixels, ignore the drag event. * + * @property {Number} [springStiffness=5.0] + * + * @property {Number} [animationTime=1.2] + * Specifies the animation duration per each {@link OpenSeadragon.Spring} + * which occur when the image is dragged or zoomed. + * + * @property {OpenSeadragon.GestureSettings} [gestureSettingsMouse] + * Settings for gestures generated by a mouse pointer device. (See {@link OpenSeadragon.GestureSettings}) + * @property {Boolean} [gestureSettingsMouse.scrollToZoom=true] - Zoom on scroll gesture + * @property {Boolean} [gestureSettingsMouse.clickToZoom=true] - Zoom on click gesture + * @property {Boolean} [gestureSettingsMouse.pinchToZoom=false] - Zoom on pinch gesture + * @property {Boolean} [gestureSettingsMouse.flickEnabled=false] - Enable 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.pinchToZoom=true] - Zoom on pinch gesture + * @property {Boolean} [gestureSettingsTouch.flickEnabled=true] - Enable 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.pinchToZoom=false] - Zoom on pinch gesture + * @property {Boolean} [gestureSettingsPen.flickEnabled=false] - Enable 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.pinchToZoom=true] - Zoom on pinch 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. + * 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). * * @property {Number} [zoomPerScroll=1.2] - * The "zoom distance" per mouse scroll or touch pinch. Note: Setting this to 1.0 effectively disables the mouse-wheel zoom feature. + * The "zoom distance" per mouse scroll or touch pinch. Note: Setting this to 1.0 effectively disables the mouse-wheel zoom feature (also see gestureSettings[Mouse|Touch|Pen].scrollToZoom}). * * @property {Number} [zoomPerSecond=1.0] * The number of seconds to animate a single zoom event over. @@ -486,6 +522,34 @@ * */ + /** + * Settings for gestures generated by a pointer device. + * + * @typedef {Object} GestureSettings + * @memberof OpenSeadragon + * + * @property {Boolean} scrollToZoom + * Set to false to disable zooming on scroll gestures. + * + * @property {Boolean} clickToZoom + * Set to false to disable zooming on click gestures. + * + * @property {Boolean} pinchToZoom + * Set to false to disable zooming on pinch gestures. + * + * @property {Boolean} flickEnabled + * Set to false to disable the kinetic panning effect (flick) at the end of a drag gesture. + * + * @property {Number} flickMinSpeed + * 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 + * 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". + * Note: springStiffness and animationTime also affect the "spring" used to stop the flick animation. + * + */ + /** * The names for the image resources used for the image navigation buttons. * @@ -825,13 +889,17 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ maxZoomLevel: null, //UI RESPONSIVENESS AND FEEL - springStiffness: 7.0, clickTimeThreshold: 300, clickDistThreshold: 5, + springStiffness: 5.0, + animationTime: 1.2, + gestureSettingsMouse: { scrollToZoom: true, clickToZoom: true, pinchToZoom: false, flickEnabled: false, flickMinSpeed: 20, flickMomentum: 0.40 }, + gestureSettingsTouch: { scrollToZoom: false, clickToZoom: false, pinchToZoom: true, flickEnabled: true, flickMinSpeed: 20, flickMomentum: 0.40 }, + gestureSettingsPen: { scrollToZoom: false, clickToZoom: true, pinchToZoom: false, flickEnabled: false, flickMinSpeed: 20, flickMomentum: 0.40 }, + gestureSettingsUnknown: { scrollToZoom: false, clickToZoom: false, pinchToZoom: true, flickEnabled: true, flickMinSpeed: 20, flickMomentum: 0.40 }, zoomPerClick: 2, zoomPerScroll: 1.2, zoomPerSecond: 1.0, - animationTime: 1.2, blendTime: 0, alwaysBlend: false, autoHideControls: true, @@ -1123,6 +1191,21 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ }, + /** + * Determines if a point is within the bounding rectangle of the given element (hit-test). + * @function + * @param {Element|String} element + * @param {OpenSeadragon.Point} point + * @returns {Boolean} + */ + pointInElement: function( element, point ) { + element = $.getElement( element ); + var offset = $.getElementOffset( element ), + size = $.getElementSize( element ); + return point.x >= offset.x && point.x < offset.x + size.x && point.y < offset.y + size.y && point.y >= offset.y; + }, + + /** * Gets the latest event, really only useful internally since its * specific to IE behavior. @@ -1586,9 +1669,6 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ return function ( element, eventName, handler, useCapture ) { element = $.getElement( element ); element.attachEvent( 'on' + eventName, handler ); - if ( useCapture && element.setCapture ) { - element.setCapture(); - } }; } else { throw new Error( "No known event model." ); @@ -1615,9 +1695,6 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){ return function( element, eventName, handler, useCapture ) { element = $.getElement( element ); element.detachEvent( 'on' + eventName, handler ); - if ( useCapture && element.releaseCapture ) { - element.releaseCapture(); - } }; } else { throw new Error( "No known event model." ); diff --git a/src/viewer.js b/src/viewer.js index a77cd46f..bd01afe3 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -275,7 +275,13 @@ $.Viewer = function( options ) { style.position = "absolute"; style.top = "0px"; style.left = "0px"; - }( this.canvas.style )); + // Disable browser default touch handling + if (style["touch-action"] !== undefined) { + style["touch-action"] = "none"; + } else if (style["-ms-touch-action"] !== undefined) { + style["-ms-touch-action"] = "none"; + } + }(this.canvas.style)); //the container is created through applying the ControlDock constructor above this.container.className = "openseadragon-container"; @@ -384,8 +390,10 @@ $.Viewer = function( options ) { clickDistThreshold: this.clickDistThreshold, clickHandler: $.delegate( this, onCanvasClick ), dragHandler: $.delegate( this, onCanvasDrag ), + dragEndHandler: $.delegate( this, onCanvasDragEnd ), releaseHandler: $.delegate( this, onCanvasRelease ), - scrollHandler: $.delegate( this, onCanvasScroll ) + scrollHandler: $.delegate( this, onCanvasScroll ), + pinchHandler: $.delegate( this, onCanvasPinch ) }).setTracking( this.mouseNavEnabled ? true : false ); // default state this.outerTracker = new $.MouseTracker({ @@ -1770,7 +1778,27 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, div.parentNode.removeChild(div); delete this.messageDiv; } + }, + + /** + * 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", etc.). + * @return {OpenSeadragon.GestureSettings} + */ + gestureSettingsByDeviceType: function ( type ) { + switch ( type ) { + case 'mouse': + return this.gestureSettingsMouse; + case 'touch': + return this.gestureSettingsTouch; + case 'pen': + return this.gestureSettingsPen; + default: + return this.gestureSettingsUnknown; + } } + }); @@ -2204,16 +2232,17 @@ function onBlur(){ } function onCanvasClick( event ) { - var zoomPerClick, - factor; - if ( !event.preventDefaultAction && this.viewport && event.quick ) { // ignore clicks where mouse moved - zoomPerClick = this.zoomPerClick; - factor = event.shift ? 1.0 / zoomPerClick : zoomPerClick; - this.viewport.zoomBy( - factor, - this.viewport.pointFromPixel( event.position, true ) - ); - this.viewport.applyConstraints(); + var gestureSettings; + + if ( !event.preventDefaultAction && this.viewport && event.quick ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.clickToZoom ) { + this.viewport.zoomBy( + event.shift ? 1.0 / this.zoomPerClick : this.zoomPerClick, + this.viewport.pointFromPixel( event.position, true ) + ); + this.viewport.applyConstraints(); + } } /** * Raised when a mouse press/release or touch/remove occurs on the {@link OpenSeadragon.Viewer#canvas} element. @@ -2239,19 +2268,18 @@ function onCanvasClick( event ) { } function onCanvasDrag( event ) { + var gestureSettings; + if ( !event.preventDefaultAction && this.viewport ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); if( !this.panHorizontal ){ event.delta.x = 0; } if( !this.panVertical ){ event.delta.y = 0; } - this.viewport.panBy( - this.viewport.deltaPointsFromPixels( - event.delta.negate() - ) - ); - if( this.constrainDuringPan ){ + this.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta.negate() ), gestureSettings.flickEnabled ); + if( this.constrainDuringPan && !gestureSettings.flickEnabled ){ this.viewport.applyConstraints(); } } @@ -2265,6 +2293,8 @@ function onCanvasDrag( 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 {OpenSeadragon.Point} delta - The x,y components of the difference between start drag and end drag. + * @property {Number} speed - 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 {Boolean} shift - True if the shift key was pressed during this event. * @property {Object} originalEvent - The original DOM event. * @property {?Object} userData - Arbitrary subscriber-defined object. @@ -2273,14 +2303,61 @@ function onCanvasDrag( event ) { tracker: event.eventSource, position: event.position, delta: event.delta, + speed: event.speed, + direction: event.direction, + shift: event.shift, + originalEvent: event.originalEvent + }); +} + +function onCanvasDragEnd( event ) { + var gestureSettings; + + if ( !event.preventDefaultAction && this.viewport ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.flickEnabled && event.speed >= gestureSettings.flickMinSpeed && !event.preventDefaultAction && this.viewport ) { + var amplitudeX = gestureSettings.flickMomentum * ( event.speed * Math.cos( event.direction ) ), + amplitudeY = gestureSettings.flickMomentum * ( event.speed * Math.sin( event.direction ) ), + center = this.viewport.pixelFromPoint( this.viewport.getCenter( true ) ), + target = this.viewport.pointFromPixel( new $.Point( center.x - amplitudeX, center.y - amplitudeY ) ); + this.viewport.panTo( target, false ); + this.viewport.applyConstraints(); + } + } + /** + * Raised when a mouse or touch drag operation ends on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-drag-end + * @memberof OpenSeadragon.Viewer + * @type {object} + * @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} speed - Speed at the end of a drag gesture, in pixels per second. + * @property {Number} 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. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-drag-end', { + tracker: event.eventSource, + position: event.position, + speed: event.speed, + direction: event.direction, shift: event.shift, originalEvent: event.originalEvent }); } function onCanvasRelease( event ) { + var gestureSettings; + if ( event.insideElementPressed && this.viewport ) { - this.viewport.applyConstraints(); + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + + if ( !gestureSettings.flickEnabled ) { + this.viewport.applyConstraints(); + } } /** * Raised when the mouse button is released or touch ends on the {@link OpenSeadragon.Viewer#canvas} element. @@ -2305,18 +2382,67 @@ function onCanvasRelease( event ) { }); } -function onCanvasScroll( event ) { - var factor; +function onCanvasPinch( event ) { + var gestureSettings; + if ( !event.preventDefaultAction && this.viewport ) { - factor = Math.pow( this.zoomPerScroll, event.scroll ); - this.viewport.zoomBy( - factor, - this.viewport.pointFromPixel( event.position, true ) - ); - this.viewport.applyConstraints(); + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.pinchToZoom ) { + var centerPt = this.viewport.pointFromPixel( event.center, true ), + lastCenterPt = this.viewport.pointFromPixel( event.lastCenter, true ); + this.viewport.zoomBy( event.distance / event.lastDistance, centerPt, true ); + this.viewport.panBy( lastCenterPt.minus( centerPt ), true ); + this.viewport.applyConstraints(); + } } /** - * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel, touch pinch, etc.). + * Raised when a pinch event occurs on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-pinch + * @memberof OpenSeadragon.Viewer + * @type {object} + * @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 {Array.} gesturePoints - Gesture points associated with the gesture. Velocity data can be found here. + * @property {OpenSeadragon.Point} lastCenter - The previous center point of the two pinch contact points relative to the tracked element. + * @property {OpenSeadragon.Point} center - The center point of the two pinch contact points relative to the tracked element. + * @property {Number} lastDistance - The previous distance between the two pinch contact points in CSS pixels. + * @property {Number} distance - The distance between the two pinch contact points in CSS pixels. + * @property {Boolean} shift - True if the shift key was pressed during this event. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent('canvas-pinch', { + tracker: event.eventSource, + gesturePoints: event.gesturePoints, + lastCenter: event.lastCenter, + center: event.center, + lastDistance: event.lastDistance, + distance: event.distance, + shift: event.shift, + originalEvent: event.originalEvent + }); + //cancels event + return false; +} + +function onCanvasScroll( event ) { + var gestureSettings, + factor; + + if ( !event.preventDefaultAction && this.viewport ) { + gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); + if ( gestureSettings.scrollToZoom ) { + factor = Math.pow( this.zoomPerScroll, event.scroll ); + this.viewport.zoomBy( + factor, + this.viewport.pointFromPixel( event.position, true ) + ); + this.viewport.applyConstraints(); + } + } + /** + * Raised when a scroll event occurs on the {@link OpenSeadragon.Viewer#canvas} element (mouse wheel). * * @event canvas-scroll * @memberof OpenSeadragon.Viewer @@ -2356,14 +2482,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 @@ -2412,14 +2540,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..61137046 100644 --- a/test/events.js +++ b/test/events.js @@ -25,184 +25,380 @@ } ); // ---------- - 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; + simEvent = {}, + offset = $canvas.offset(), + tracker = viewer.innerTracker, + intervalId, + origEnterHandler, + origExitHandler, + origPressHandler, + origReleaseHandler, + origMoveHandler, + origClickHandler, + origDragHandler, + origDragEndHandler, + enterCount, + exitCount, + pressCount, + releaseCount, + moveCount, + clickCount, + dragCount, + dragEndCount, + insideElementPressed, + insideElementReleased, + quickClick, + speed, + direction; + + 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++; + speed = event.speed; + direction = event.direction; + 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 (x, y) { + simEvent.clientX = offset.left + x; + simEvent.clientY = offset.top + y; + $canvas.simulate( OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseenter' : 'mouseover', simEvent ); + }; + + var simulateLeave = function (x, y) { + simEvent.clientX = offset.left + x; + simEvent.clientY = offset.top + y; + $canvas.simulate( OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseleave' : 'mouseout', simEvent ); + }; + + var simulateDown = function (x, y) { + simEvent.clientX = offset.left + x; + simEvent.clientY = offset.top + y; + $canvas.simulate( 'mousedown', simEvent ); + }; + + var simulateUp = function (x, y) { + simEvent.clientX = offset.left + x; + simEvent.clientY = offset.top + y; + $canvas.simulate( 'mouseup', simEvent ); + }; + + var simulateMove = function (dX, dY, count) { + var i; + for ( i = 0; i < count; i++ ) { + simEvent.clientX += dX; + simEvent.clientY += dY; + $canvas.simulate( 'mousemove', simEvent ); + } + }; + + var resetForAssessment = function () { + simEvent = { + clientX: offset.left, + clientY: offset.top + }; + enterCount = 0; + exitCount = 0; + pressCount = 0; + releaseCount = 0; + moveCount = 0; + clickCount = 0; + dragCount = 0; + dragEndCount = 0; + insideElementPressed = false; + insideElementReleased = false; + quickClick = false; + speed = 0; + direction = 2 * Math.PI; + }; + + 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 + ')' ); + } + if ('speed' in expected) { + Util.assessNumericValue(expected.speed, speed, 1.0, expected.description + 'Drag speed '); + } + if ('direction' in expected) { + Util.assessNumericValue(expected.direction, direction, 0.2, expected.description + 'Drag direction '); + } + }; var onOpen = function ( event ) { + var timeStart, + timeElapsed; + 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(); + simulateEnter(0, 0); + simulateMove(1, 1, 10); + simulateMove(-1, -1, 10); + simulateUp(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(-1, -1); // flush tracked pointer - var event = { - clientX:1, - clientY:1 - }; + // enter-move-exit (fly-over) + resetForAssessment(); + simulateEnter(0, 0); + simulateMove(1, 1, 10); + simulateMove(-1, -1, 10); + simulateLeave(-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(); + simulateMove(1, 1, 10); + simulateMove(-1, -1, 10); + simulateLeave(-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(); + simulateEnter(0, 0); + simulateDown(0, 0); + simulateUp(0, 0); + simulateLeave(-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(); + simulateEnter(0, 0); + simulateDown(0, 0); + simulateMove(1, 1, 100); + simulateUp(10, 10); + simulateMove(-1, -1, 100); + simulateLeave(-1, -1); + assessGestureExpectations({ + description: 'enter-press-move-release-move-exit (drag, release in tracked element): ', + enterCount: 1, + exitCount: 1, + pressCount: 1, + releaseCount: 1, + moveCount: 200, + clickCount: 1, + dragCount: 100, + 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(); + simulateEnter(0, 0); + simulateDown(0, 0); + simulateMove(1, 1, 5); + simulateMove(-1, -1, 5); + simulateLeave(-1, -1); + simulateMove(-1, -1, 5); + simulateUp(-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 +409,7 @@ } ); // ---------- - asyncTest( 'MouseTracker preventDefaultAction', function () { + asyncTest( 'Viewer: preventDefaultAction', function () { var $canvas = $( viewer.element ).find( '.openseadragon-canvas' ).not( '.navigator .openseadragon-canvas' ), tracker = viewer.innerTracker, origClickHandler, @@ -281,7 +477,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( 'EventSource: 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( 'EventSource: 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 new file mode 100644 index 00000000..9643d610 --- /dev/null +++ b/test/legacy.mouse.shim.js @@ -0,0 +1,42 @@ +(function($, undefined) { + + /** + * Plugin to force OpenSeadragon to use the legacy mouse pointer event model + */ + + $.MouseTracker.subscribeEvents = [ "click", "keypress", "focus", "blur", $.MouseTracker.wheelEventName ]; + + if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) { + // Older Firefox + $.MouseTracker.subscribeEvents.push( "MozMousePixelScroll" ); + } + + $.MouseTracker.subscribeEvents.push( "mousedown", "mouseup", "mousemove" ); + if ( 'onmouseenter' in window ) { + $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave" ); + $.MouseTracker.haveMouseEnter = true; + } else { + $.MouseTracker.subscribeEvents.push( "mouseover", "mouseout" ); + $.MouseTracker.haveMouseEnter = false; + } + 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 { + $.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) + // Subscribe to these to prevent default gesture handling + $.MouseTracker.subscribeEvents.push( "gesturestart", "gesturechange" ); + } + $.MouseTracker.mousePointerId = "legacy-mouse"; + $.MouseTracker.maxTouchPoints = 10; + +}(OpenSeadragon)); diff --git a/test/navigator.js b/test/navigator.js index 95631a91..c59d528a 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(OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseenter' : 'mouseover', event) .simulate('mousedown', event) .simulate('mouseup', event); }; diff --git a/test/test.html b/test/test.html index 21802fa1..64062197 100644 --- a/test/test.html +++ b/test/test.html @@ -1,4 +1,4 @@ - + @@ -15,6 +15,7 @@ +