diff --git a/Gruntfile.js b/Gruntfile.js index e030188c..e3395308 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -202,7 +202,7 @@ module.exports = function(grunt) { target: sources }, "git-describe": { - "options": { + options: { failOnError: false }, build: {} diff --git a/changelog.txt b/changelog.txt index c079381d..38fcc2d4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,15 +1,41 @@ OPENSEADRAGON CHANGELOG ======================= -2.4.3: (In progress) +2.5.0: (In progress) +* BREAKING CHANGE: Dropped support for older browsers (IE < 11) (#1949 @msalsbery) +* BREAKING CHANGE: Removed deprecated OpenSeadragon.getEvent function (#1949 @msalsbery) +* DEPRECATION: MouseTracker exitHandler deprecated for name change to leaveHandler for consistency with DOM event names (#1872 @msalsbery) * Now when "simple image" tile sources are removed from the viewer, they free the memory used by the pyramid they create (#1789 @TakumaKira) * Documentation fix (#1814 @kenanchristian) * Better cleanup on destruction, to avoid memory leaks (#1832 @JoFrMueller) +* Better handle destruction when navigator in custom location (#1884 @woodchuck) * Miscellaneous code cleanup (#1840 @msalsbery) * You can now specify tileSize for the Zoomify Tile Source (#1868 @abrlam) * Better use of IIIF "max" and "full" URL parameters (#1871 @MImranAsghar) * You can now specify the file format of the tiles in the Zoomify tile source (#1889 @abrlam) +* Improved browser sniffing - detect EDGE and CHROMEEDGE browsers (#1872 @msalsbery) +* Improved DOM event model feature detection (#1872 @msalsbery) +* Added support for options parameter on addEvent()/removeEvent (to support passive option) (#1872 @msalsbery) +* Added OpenSeadragon.eventIsCanceled() function for defaultPrevented detection on DOM events (#1872 @msalsbery) +* MouseTracker: better PointerEvent model detection - removed use of deprecated window.navigator.pointerEnabled (#1872 @msalsbery) +* MouseTracker: added overHandler/outHandler options for handling corresponding pointerover/pointerout events (#1872 @msalsbery) +* MouseTracker: changed enterHandler/leaveHandler to use DOM pointerenter/pointerleave events instead of simulating using pointerover/pointerout (#1872 @msalsbery) +* All internal uses of MouseTracker use pointerenter/pointerleave events instead of pointerover/pointerout events for more consistent pointer tracking (#1872 @msalsbery) +* Fixed bug in Button class where two MouseTracker event handlers used an invalid "this" causing issues in some browsers (#1872 @msalsbery) +* Added pointerType property to Viewer container-enter, container-exit, canvas-drag, canvas-drag-end, canvas-pinch events (#1872 @msalsbery) +* MouseTracker: Fire dragEndHandler event even if release point same as initial contact point (#1872 @msalsbery) +* MouseTracker: Pointer capture implemented with capture APIs where available. Only fallback to emulated capture on extremely old browsers (#1872 @msalsbery) +* MouseTracker: Added preProcessEventHandler option to allow MouseTracker instances to control bubbling and default behavior of events on their associated element (#1872 @msalsbery) +* MouseTracker: Improved handling of canceled events (#1872 @msalsbery) +* MouseTracker: Improved releasing of tracked pointers on destroy()/stopTracking() (#1872 @msalsbery) +* Updated Viewer, Button, Drawer, Navigator, ReferenceStrip DOM for proper DOM event handling (#1872 @msalsbery) +* Added OpenSeadragon.setElementPointerEventsNone() for setting pointer-events:'none' on DOM elements (#1872 @msalsbery) +* MouseTracker: added contextMenuHandler option for handling contextmenu events (#1872 @msalsbery) +* Viewer: added a canvas-contextmenu event (#1872 @msalsbery) +* Added additional documentation for the zoomPerSecond viewer option (#1872 @msalsbery) +* MouseTracker: Per #1863, dropped support for Internet Explorer < 11 (#1872 @msalsbery) (#1950 @rmontroy) +* Fixed simulated drag events in navigator tests (#1949 @msalsbery) 2.4.2: @@ -57,7 +83,7 @@ OPENSEADRAGON CHANGELOG * You can now prevent canvas-click events on the navigator (#1416) * The navigator can now be restricted to just horizontal or just vertical panning (#1416) * Fixed DziTileSource so it doesn't load levels above maxLevel or below minLevel, if set (#1492) - + 2.3.1: * Debug mode now uses different colors for different tiled images (customizable via debugGridColor) (#1271) diff --git a/package-lock.json b/package-lock.json index 04ffb707..96715f72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -389,9 +389,9 @@ } }, "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, "requires": { "readable-stream": "^2.3.5", @@ -2578,9 +2578,9 @@ "dev": true }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", "dev": true }, "is-accessor-descriptor": { @@ -4938,9 +4938,9 @@ }, "dependencies": { "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "optional": true, "requires": { diff --git a/src/button.js b/src/button.js index 7b00f9ee..5eaabfc3 100644 --- a/src/button.js +++ b/src/button.js @@ -77,6 +77,7 @@ $.ButtonState = { * @param {OpenSeadragon.EventHandler} [options.onExit=null] Event handler callback for {@link OpenSeadragon.Button.event:exit}. * @param {OpenSeadragon.EventHandler} [options.onFocus=null] Event handler callback for {@link OpenSeadragon.Button.event:focus}. * @param {OpenSeadragon.EventHandler} [options.onBlur=null] Event handler callback for {@link OpenSeadragon.Button.event:blur}. + * @param {Object} [options.userData=null] Arbitrary object to be passed unchanged to any attached handler methods. */ $.Button = function( options ) { @@ -111,7 +112,8 @@ $.Button = function( options ) { onEnter: null, onExit: null, onFocus: null, - onBlur: null + onBlur: null, + userData: null }, options ); @@ -136,6 +138,13 @@ $.Button = function( options ) { this.imgDown.alt = this.tooltip; + // Allow pointer events to pass through the img elements so implicit + // pointer capture works on touch devices + $.setElementPointerEventsNone( this.imgRest ); + $.setElementPointerEventsNone( this.imgGroup ); + $.setElementPointerEventsNone( this.imgHover ); + $.setElementPointerEventsNone( this.imgDown ); + this.element.style.position = "relative"; $.setElementTouchActionNone( this.element ); @@ -203,6 +212,7 @@ $.Button = function( options ) { */ this.tracker = new $.MouseTracker({ + userData: 'Button.tracker', element: this.element, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, @@ -227,7 +237,7 @@ $.Button = function( options ) { }, focusHandler: function ( event ) { - this.enterHandler( event ); + _this.tracker.enterHandler( event ); /** * Raised when the Button element receives focus. * @@ -241,7 +251,7 @@ $.Button = function( options ) { _this.raiseEvent( "focus", { originalEvent: event.originalEvent } ); }, - exitHandler: function( event ) { + leaveHandler: function( event ) { outTo( _this, $.ButtonState.GROUP ); if ( event.insideElementPressed ) { /** @@ -259,7 +269,7 @@ $.Button = function( options ) { }, blurHandler: function ( event ) { - this.exitHandler( event ); + _this.tracker.leaveHandler( event ); /** * Raised when the Button element loses focus. * @@ -363,8 +373,8 @@ $.Button = function( options ) { $.extend( $.Button.prototype, $.EventSource.prototype, /** @lends OpenSeadragon.Button.prototype */{ /** - * TODO: Determine what this function is intended to do and if it's actually - * useful as an API point. + * Used by a button container element (e.g. a ButtonGroup) to transition the button state + * to ButtonState.GROUP. * @function */ notifyGroupEnter: function() { @@ -372,8 +382,8 @@ $.extend( $.Button.prototype, $.EventSource.prototype, /** @lends OpenSeadragon. }, /** - * TODO: Determine what this function is intended to do and if it's actually - * useful as an API point. + * Used by a button container element (e.g. a ButtonGroup) to transition the button state + * to ButtonState.REST. * @function */ notifyGroupExit: function() { @@ -399,14 +409,22 @@ $.extend( $.Button.prototype, $.EventSource.prototype, /** @lends OpenSeadragon. }, destroy: function() { - this.element.removeChild(this.imgRest); - this.imgRest = null; - this.element.removeChild(this.imgGroup); - this.imgGroup = null; - this.element.removeChild(this.imgHover); - this.imgHover = null; - this.element.removeChild(this.imgDown); - this.imgDown = null; + if (this.imgRest) { + this.element.removeChild(this.imgRest); + this.imgRest = null; + } + if (this.imgGroup) { + this.element.removeChild(this.imgGroup); + this.imgGroup = null; + } + if (this.imgHover) { + this.element.removeChild(this.imgHover); + this.imgHover = null; + } + if (this.imgDown) { + this.element.removeChild(this.imgDown); + this.imgDown = null; + } this.removeAllHandlers(); this.tracker.destroy(); this.element = null; diff --git a/src/buttongroup.js b/src/buttongroup.js index 995f90d3..5af71fea 100644 --- a/src/buttongroup.js +++ b/src/buttongroup.js @@ -88,6 +88,7 @@ $.ButtonGroup = function( options ) { * @memberof OpenSeadragon.ButtonGroup# */ this.tracker = new $.MouseTracker({ + userData: 'ButtonGroup.tracker', element: this.element, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, @@ -97,7 +98,7 @@ $.ButtonGroup = function( options ) { _this.buttons[ i ].notifyGroupEnter(); } }, - exitHandler: function ( event ) { + leaveHandler: function ( event ) { var i; if ( !event.insideElementPressed ) { for ( i = 0; i < _this.buttons.length; i++ ) { @@ -127,8 +128,8 @@ $.ButtonGroup.prototype = { * @function * @private */ - emulateExit: function() { - this.tracker.exitHandler( { eventSource: this.tracker } ); + emulateLeave: function() { + this.tracker.leaveHandler( { eventSource: this.tracker } ); }, destroy: function() { diff --git a/src/control.js b/src/control.js index 64f762d7..6cb229f8 100644 --- a/src/control.js +++ b/src/control.js @@ -161,7 +161,9 @@ $.Control.prototype = { */ destroy: function() { this.wrapper.removeChild( this.element ); - this.container.removeChild( this.wrapper ); + if (this.anchor !== $.ControlAnchor.NONE) { + this.container.removeChild(this.wrapper); + } }, /** diff --git a/src/drawer.js b/src/drawer.js index 31968224..fca4b956 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -126,6 +126,10 @@ $.Drawer = function( options ) { this.canvas.style.height = "100%"; this.canvas.style.position = "absolute"; $.setElementOpacity( this.canvas, this.opacity, true ); + // Allow pointer events to pass through the canvas element so implicit + // pointer capture works on touch devices + $.setElementPointerEventsNone( this.canvas ); + $.setElementTouchActionNone( this.canvas ); // explicit left-align this.container.style.textAlign = "left"; diff --git a/src/mousetracker.js b/src/mousetracker.js index d1b584ae..4aefd249 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -72,10 +72,20 @@ * @param {Number} [options.stopDelay=50] * The number of milliseconds without pointer move before the stop * event is fired. + * @param {OpenSeadragon.EventHandler} [options.preProcessEventHandler=null] + * An optional handler for controlling DOM event propagation and processing. + * @param {OpenSeadragon.EventHandler} [options.contextMenuHandler=null] + * An optional handler for contextmenu. * @param {OpenSeadragon.EventHandler} [options.enterHandler=null] * An optional handler for pointer enter. + * @param {OpenSeadragon.EventHandler} [options.leaveHandler=null] + * An optional handler for pointer leave. * @param {OpenSeadragon.EventHandler} [options.exitHandler=null] - * An optional handler for pointer exit. + * An optional handler for pointer leave. Deprecated. Use leaveHandler instead. + * @param {OpenSeadragon.EventHandler} [options.overHandler=null] + * An optional handler for pointer over. + * @param {OpenSeadragon.EventHandler} [options.outHandler=null] + * An optional handler for pointer out. * @param {OpenSeadragon.EventHandler} [options.pressHandler=null] * An optional handler for pointer press. * @param {OpenSeadragon.EventHandler} [options.nonPrimaryPressHandler=null] @@ -164,8 +174,13 @@ this.userData = options.userData || null; this.stopDelay = options.stopDelay || 50; + this.preProcessEventHandler = options.preProcessEventHandler || null; + this.contextMenuHandler = options.contextMenuHandler || null; this.enterHandler = options.enterHandler || null; - this.exitHandler = options.exitHandler || null; + this.leaveHandler = options.leaveHandler || null; + this.exitHandler = options.exitHandler || null; // Deprecated v2.5.0 + this.overHandler = options.overHandler || null; + this.outHandler = options.outHandler || null; this.pressHandler = options.pressHandler || null; this.nonPrimaryPressHandler = options.nonPrimaryPressHandler || null; this.releaseHandler = options.releaseHandler || null; @@ -201,44 +216,41 @@ keypress: function ( event ) { onKeyPress( _this, event ); }, focus: function ( event ) { onFocus( _this, event ); }, blur: function ( event ) { onBlur( _this, event ); }, + contextmenu: function ( event ) { onContextMenu( _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 ); }, - mouseenter: function ( event ) { onMouseEnter( _this, event ); }, // Used on IE8 only - mouseleave: function ( event ) { onMouseLeave( _this, event ); }, // Used on IE8 only - mouseover: function ( event ) { onMouseOver( _this, event ); }, - mouseout: function ( event ) { onMouseOut( _this, event ); }, - mousedown: function ( event ) { onMouseDown( _this, event ); }, - mouseup: function ( event ) { onMouseUp( _this, event ); }, - mouseupcaptured: function ( event ) { onMouseUpCaptured( _this, event ); }, - mousemove: function ( event ) { onMouseMove( _this, event ); }, - mousemovecaptured: function ( event ) { onMouseMoveCaptured( _this, event ); }, + losecapture: function ( event ) { onLoseCapture( _this, event ); }, + + mouseenter: function ( event ) { onPointerEnter( _this, event ); }, + mouseleave: function ( event ) { onPointerLeave( _this, event ); }, + mouseover: function ( event ) { onPointerOver( _this, event ); }, + mouseout: function ( event ) { onPointerOut( _this, event ); }, + mousedown: function ( event ) { onPointerDown( _this, event ); }, + mouseup: function ( event ) { onPointerUp( _this, event ); }, + mousemove: function ( event ) { onPointerMove( _this, event ); }, touchstart: function ( event ) { onTouchStart( _this, event ); }, touchend: function ( event ) { onTouchEnd( _this, event ); }, - touchendcaptured: function ( event ) { onTouchEndCaptured( _this, event ); }, touchmove: function ( event ) { onTouchMove( _this, event ); }, - touchmovecaptured: function ( event ) { onTouchMoveCaptured( _this, event ); }, touchcancel: function ( event ) { onTouchCancel( _this, event ); }, - gesturestart: function ( event ) { onGestureStart( _this, event ); }, - gesturechange: function ( event ) { onGestureChange( _this, event ); }, + gesturestart: function ( event ) { onGestureStart( _this, event ); }, // Safari/Safari iOS + gesturechange: function ( event ) { onGestureChange( _this, event ); }, // Safari/Safari iOS + gotpointercapture: function ( event ) { onGotPointerCapture( _this, event ); }, + lostpointercapture: function ( event ) { onLostPointerCapture( _this, event ); }, + pointerenter: function ( event ) { onPointerEnter( _this, event ); }, + pointerleave: function ( event ) { onPointerLeave( _this, event ); }, pointerover: function ( event ) { onPointerOver( _this, event ); }, - MSPointerOver: function ( event ) { onPointerOver( _this, event ); }, pointerout: function ( event ) { onPointerOut( _this, event ); }, - MSPointerOut: function ( event ) { onPointerOut( _this, event ); }, 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 ); }, pointerupcaptured: function ( event ) { onPointerUpCaptured( _this, event ); }, pointermovecaptured: function ( event ) { onPointerMoveCaptured( _this, event ); }, @@ -262,6 +274,18 @@ currentPinchCenter: null }; + this.hasGestureHandlers = !!( this.pressHandler || this.nonPrimaryPressHandler || + this.releaseHandler || this.nonPrimaryReleaseHandler || + this.clickHandler || this.dblClickHandler || + this.dragHandler || this.dragEndHandler || + this.pinchHandler ); + this.hasScrollHandler = !!this.scrollHandler; + this.hasContextMenuHandler = !!this.contextMenuHandler; + + if (this.exitHandler) { + $.console.error("MouseTracker.exitHandler is deprecated. Use MouseTracker.leaveHandler instead."); + } + if ( !options.startDisabled ) { this.setTracking( true ); } @@ -317,25 +341,6 @@ return this; }, - /** - * Returns the {@link OpenSeadragon.MouseTracker.GesturePointList|GesturePointList} for all but the given pointer device type. - * @function - * @param {String} type - The pointer device type: "mouse", "touch", "pen", etc. - * @returns {Array.} - */ - getActivePointersListsExceptType: function ( type ) { - var delegate = THIS[ this.hash ]; - var listArray = []; - - for (var i = 0; i < delegate.activePointersLists.length; ++i) { - if (delegate.activePointersLists[i].type !== type) { - listArray.push(delegate.activePointersLists[i]); - } - } - - return listArray; - }, - /** * 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. @@ -378,6 +383,30 @@ return count; }, + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + */ + preProcessEventHandler: 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 {OpenSeadragon.Point} event.position + * The position of the event relative to the tracked element. + * @param {Object} event.originalEvent + * The original event object. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + contextMenuHandler: function () { }, + /** * Implement or assign implementation to these handlers during or after * calling the constructor. @@ -403,8 +432,6 @@ * 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. */ @@ -414,6 +441,38 @@ * Implement or assign implementation to these handlers during or after * calling the constructor. * @function + * @since v2.5.0 + * @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 {Number} event.pointers + * Number of pointers (all types) active in the tracked element. + * @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. Deprecated. Use buttons instead. + * @param {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. + * @param {Object} event.originalEvent + * The original event object. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + leaveHandler: function () { }, + + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @deprecated v2.5.0 Use leaveHandler instead * @param {Object} event * @param {OpenSeadragon.MouseTracker} event.eventSource * A reference to the tracker instance. @@ -442,6 +501,72 @@ */ exitHandler: function () { }, + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @since v2.5.0 + * @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 {Number} event.pointers + * Number of pointers (all types) active in the tracked element. + * @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. Deprecated. Use buttons instead. + * @param {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. + * @param {Object} event.originalEvent + * 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. + */ + overHandler: function () { }, + + /** + * Implement or assign implementation to these handlers during or after + * calling the constructor. + * @function + * @since v2.5.0 + * @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 {Number} event.pointers + * Number of pointers (all types) active in the tracked element. + * @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. Deprecated. Use buttons instead. + * @param {Boolean} event.isTouchEvent + * True if the original event is a touch event, otherwise false. Deprecated. Use pointerType and/or originalEvent instead. + * @param {Object} event.originalEvent + * 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. + */ + outHandler: function () { }, + /** * Implement or assign implementation to these handlers during or after * calling the constructor. @@ -881,21 +1006,35 @@ blurHandler: function () { } }; + // https://github.com/openseadragon/openseadragon/pull/790 /** - * Resets all active mousetrakers. (Added to patch issue #697 "Mouse up outside map will cause "canvas-drag" event to stick") - * + * True if inside an iframe, otherwise false. + * @member {Boolean} isInIframe * @private - * @member resetAllMouseTrackers - * @memberof OpenSeadragon.MouseTracker + * @inner */ - $.MouseTracker.resetAllMouseTrackers = function(){ - for(var i = 0; i < MOUSETRACKERS.length; i++){ - if (MOUSETRACKERS[i].isTracking()){ - MOUSETRACKERS[i].setTracking(false); - MOUSETRACKERS[i].setTracking(true); - } + var isInIframe = (function() { + try { + return window.self !== window.top; + } catch (e) { + return true; } - }; + })(); + + // https://github.com/openseadragon/openseadragon/pull/790 + /** + * @function + * @private + * @inner + * @returns {Boolean} True if the target supports DOM Level 2 event subscription methods, otherwise false. + */ + function canAccessEvents (target) { + try { + return target.addEventListener && target.removeEventListener; + } catch (e) { + return false; + } + } /** * Provides continuous computation of velocity (speed and direction) of active pointers. @@ -1002,58 +1141,42 @@ document.onmousewheel !== undefined ? 'mousewheel' : // Webkit and IE support at least 'mousewheel' 'DOMMouseScroll'; // Assume old Firefox - /** - * Detect legacy mouse capture support. - */ - $.MouseTracker.supportsMouseCapture = (function () { - var divElement = document.createElement( 'div' ); - return $.isFunction( divElement.setCapture ) && $.isFunction( divElement.releaseCapture ); - }()); - /** * Detect browser pointer device event model(s) and build appropriate list of events to subscribe to. */ - $.MouseTracker.subscribeEvents = [ "click", "dblclick", "keydown", "keyup", "keypress", "focus", "blur", $.MouseTracker.wheelEventName ]; + $.MouseTracker.subscribeEvents = [ "click", "dblclick", "keydown", "keyup", "keypress", "focus", "blur", "contextmenu", $.MouseTracker.wheelEventName ]; if( $.MouseTracker.wheelEventName === "DOMMouseScroll" ) { // Older Firefox $.MouseTracker.subscribeEvents.push( "MozMousePixelScroll" ); } - // Note: window.navigator.pointerEnable is deprecated on IE 11 and not part of W3C spec. - if ( window.PointerEvent && ( window.navigator.pointerEnabled || $.Browser.vendor !== $.BROWSERS.IE ) ) { + if ( window.PointerEvent ) { // IE11 and other W3C Pointer Event implementations (see http://www.w3.org/TR/pointerevents) $.MouseTracker.havePointerEvents = true; - $.MouseTracker.subscribeEvents.push( "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel" ); - $.MouseTracker.unprefixedPointerEvents = true; - if( navigator.maxTouchPoints ) { - $.MouseTracker.maxTouchPoints = navigator.maxTouchPoints; - } else { - $.MouseTracker.maxTouchPoints = 0; + $.MouseTracker.subscribeEvents.push( "pointerenter", "pointerleave", "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel" ); + // Pointer events capture support + $.MouseTracker.havePointerCapture = (function () { + var divElement = document.createElement( 'div' ); + return $.isFunction( divElement.setPointerCapture ) && $.isFunction( divElement.releasePointerCapture ); + }()); + if ( $.MouseTracker.havePointerCapture ) { + $.MouseTracker.subscribeEvents.push( "gotpointercapture", "lostpointercapture" ); } - $.MouseTracker.haveMouseEnter = false; - } else if ( window.MSPointerEvent && window.navigator.msPointerEnabled ) { - // IE10 - $.MouseTracker.havePointerEvents = true; - $.MouseTracker.subscribeEvents.push( "MSPointerOver", "MSPointerOut", "MSPointerDown", "MSPointerUp", "MSPointerMove", "MSPointerCancel" ); - $.MouseTracker.unprefixedPointerEvents = false; - if( navigator.msMaxTouchPoints ) { - $.MouseTracker.maxTouchPoints = navigator.msMaxTouchPoints; - } else { - $.MouseTracker.maxTouchPoints = 0; - } - $.MouseTracker.haveMouseEnter = false; } else { // Legacy W3C mouse events $.MouseTracker.havePointerEvents = false; - if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { - $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave" ); - $.MouseTracker.haveMouseEnter = true; - } else { - $.MouseTracker.subscribeEvents.push( "mouseover", "mouseout" ); - $.MouseTracker.haveMouseEnter = false; + $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave", "mouseover", "mouseout", "mousedown", "mouseup", "mousemove" ); + $.MouseTracker.mousePointerId = "legacy-mouse"; + // Legacy mouse events capture support (IE/Firefox only?) + $.MouseTracker.havePointerCapture = (function () { + var divElement = document.createElement( 'div' ); + return $.isFunction( divElement.setCapture ) && $.isFunction( divElement.releaseCapture ); + }()); + if ( $.MouseTracker.havePointerCapture ) { + $.MouseTracker.subscribeEvents.push( "losecapture" ); } - $.MouseTracker.subscribeEvents.push( "mousedown", "mouseup", "mousemove" ); + // Legacy touch events if ( 'ontouchstart' in window ) { // iOS, Android, and other W3c Touch Event implementations // (see http://www.w3.org/TR/touch-events/) @@ -1066,8 +1189,6 @@ // Subscribe to these to prevent default gesture handling $.MouseTracker.subscribeEvents.push( "gesturestart", "gesturechange" ); } - $.MouseTracker.mousePointerId = "legacy-mouse"; - $.MouseTracker.maxTouchPoints = 10; } @@ -1075,6 +1196,50 @@ // Classes and typedefs /////////////////////////////////////////////////////////////////////////////// + /** + * Used for the processing/disposition of DOM events (propagation, default handling, capture, etc.) + * + * @typedef {Object} EventProcessInfo + * @memberof OpenSeadragon.MouseTracker + * @since v2.5.0 + * + * @property {OpenSeadragon.MouseTracker} eventSource + * A reference to the tracker instance. + * @property {Object} originalEvent + * The original DOM event object. + * @property {Number} eventPhase + * 0 == NONE, 1 == CAPTURING_PHASE, 2 == AT_TARGET, 3 == BUBBLING_PHASE. + * @property {String} eventType + * "contextmenu", "gotpointercapture", "lostpointercapture", "pointerenter", "pointerleave", "pointerover", "pointerout", "pointerdown", "pointerup", "pointermove", "pointercancel", "wheel", "click", "dblclick". + * @property {String} pointerType + * "mouse", "touch", "pen", etc. + * @property {Boolean} isEmulated + * True if this is an emulated event. If true, originalEvent is either the event that caused + * the emulated event, a synthetic event object created with values from the actual DOM event, + * or null if no DOM event applies. Emulated events can occur on eventType "wheel" on legacy mouse-scroll + * event emitting user agents. + * @property {Boolean} isStopable + * True if propagation of the event (e.g. bubbling) can be stopped with stopPropagation/stopImmediatePropagation. + * @property {Boolean} isCancelable + * True if the event's default handling by the browser can be prevented with preventDefault. + * @property {Boolean} defaultPrevented + * True if the event's default handling has already been prevented by a descendent element. + * @property {Boolean} preventDefault + * Set to true to prevent the event's default handling by the browser. + * @property {Boolean} preventGesture + * Set to true to prevent this MouseTracker from generating a gesture from the event. + * Valid on eventType "pointerdown". + * @property {Boolean} stopPropagation + * Set to true prevent the event from propagating to ancestor/descendent elements on capture/bubble phase. + * @property {Boolean} shouldCapture + * (Internal Use) Set to true if the pointer should be captured (events (re)targeted to tracker element). + * @property {Boolean} shouldReleaseCapture + * (Internal Use) Set to true if the captured pointer should be released. + * @property {Object} userData + * Arbitrary user-defined object. + */ + + /** * Represents a point of contact on the screen made by a mouse cursor, pen, touch, or other pointer device. * @@ -1238,7 +1403,7 @@ }, /** - * Increment this pointer's contact count. + * Increment this pointer list's contact count. * It will evaluate whether this pointer type is allowed to have multiple contacts. * @function */ @@ -1246,12 +1411,13 @@ ++this.contacts; if (this.contacts > 1 && (this.type === "mouse" || this.type === "pen")) { + $.console.warn('GesturePointList.addContact() Implausible contacts value'); this.contacts = 1; } }, /** - * Decrement this pointer's contact count. + * Decrement this pointer list's contact count. * It will make sure the count does not go below 0. * @function */ @@ -1259,6 +1425,7 @@ --this.contacts; if (this.contacts < 0) { + $.console.warn('GesturePointList.removeContact() Implausible contacts value'); this.contacts = 0; } } @@ -1276,49 +1443,28 @@ */ function clearTrackedPointers( tracker ) { var delegate = THIS[ tracker.hash ], - i, + i, j, + pointsList, + gPoints, + gPointsToRemove, pointerListCount = delegate.activePointersLists.length; for ( i = 0; i < pointerListCount; i++ ) { - if ( delegate.activePointersLists[ i ].captureCount > 0 ) { - $.removeEvent( - $.MouseTracker.captureElement, - 'mousemove', - delegate.mousemovecaptured, - true - ); - $.removeEvent( - $.MouseTracker.captureElement, - 'mouseup', - delegate.mouseupcaptured, - true - ); - $.removeEvent( - $.MouseTracker.captureElement, - $.MouseTracker.unprefixedPointerEvents ? 'pointermove' : 'MSPointerMove', - delegate.pointermovecaptured, - true - ); - $.removeEvent( - $.MouseTracker.captureElement, - $.MouseTracker.unprefixedPointerEvents ? 'pointerup' : 'MSPointerUp', - delegate.pointerupcaptured, - true - ); - $.removeEvent( - $.MouseTracker.captureElement, - 'touchmove', - delegate.touchmovecaptured, - true - ); - $.removeEvent( - $.MouseTracker.captureElement, - 'touchend', - delegate.touchendcaptured, - true - ); + pointsList = delegate.activePointersLists[ i ]; - delegate.activePointersLists[ i ].captureCount = 0; + if ( pointsList.getLength() > 0 ) { + // Make an array containing references to the gPoints in the pointer list + // (because calls to stopTrackingPointer() are going to modify the pointer list) + gPointsToRemove = []; + gPoints = pointsList.asArray(); + for ( j = 0; j < gPoints.length; j++ ) { + gPointsToRemove.push( gPoints[ j ] ); + } + + // Release and remove all gPoints from the pointer list + for ( j = 0; j < gPointsToRemove.length; j++ ) { + stopTrackingPointer( tracker, pointsList, gPointsToRemove[ j ] ); + } } } @@ -1390,17 +1536,17 @@ if ( pointerType === 'pointerevent' ) { return { - upName: $.MouseTracker.unprefixedPointerEvents ? 'pointerup' : 'MSPointerUp', + upName: 'pointerup', upHandler: delegate.pointerupcaptured, - moveName: $.MouseTracker.unprefixedPointerEvents ? 'pointermove' : 'MSPointerMove', + moveName: 'pointermove', moveHandler: delegate.pointermovecaptured }; } else if ( pointerType === 'mouse' ) { return { - upName: 'mouseup', - upHandler: delegate.mouseupcaptured, - moveName: 'mousemove', - moveHandler: delegate.mousemovecaptured + upName: 'pointerup', + upHandler: delegate.pointerupcaptured, + moveName: 'pointermove', + moveHandler: delegate.pointermovecaptured }; } else if ( pointerType === 'touch' ) { return { @@ -1419,42 +1565,53 @@ * @private * @inner */ - function capturePointer( tracker, pointerType, pointerCount ) { - var pointsList = tracker.getActivePointersListByType( pointerType ), - eventParams; + function capturePointer( tracker, gPoint ) { + var eventParams; - pointsList.captureCount += (pointerCount || 1); - - if ( pointsList.captureCount === 1 ) { - if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { - tracker.element.setCapture( true ); - } else { - eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : pointerType ); - // We emulate mouse capture by hanging listeners on the document object. - // (Note we listen on the capture phase so the captured handlers will get called first) - // eslint-disable-next-line no-use-before-define - if (isInIframe && canAccessEvents(window.top)) { - $.addEvent( - window.top, - eventParams.upName, - eventParams.upHandler, - true - ); + if ( $.MouseTracker.havePointerCapture ) { + if ( $.MouseTracker.havePointerEvents ) { + // Can throw InvalidPointerId + // (should never happen so we'll log a warning) + try { + tracker.element.setPointerCapture( gPoint.id ); + //$.console.log('element.setPointerCapture() called'); + } catch ( e ) { + $.console.warn('setPointerCapture() called on invalid pointer ID'); } + } else { + tracker.element.setCapture( true ); + //$.console.log('element.setCapture() called'); + } + } else { + // Emulate mouse capture by hanging listeners on the document object. + // (Note we listen on the capture phase so the captured handlers will get called first) + // eslint-disable-next-line no-use-before-define + //$.console.log('Emulated mouse capture set'); + eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : gPoint.type ); + // https://github.com/openseadragon/openseadragon/pull/790 + if (isInIframe && canAccessEvents(window.top)) { $.addEvent( - $.MouseTracker.captureElement, + window.top, eventParams.upName, eventParams.upHandler, true ); - $.addEvent( - $.MouseTracker.captureElement, - eventParams.moveName, - eventParams.moveHandler, - true - ); } + $.addEvent( + $.MouseTracker.captureElement, + eventParams.upName, + eventParams.upHandler, + true + ); + $.addEvent( + $.MouseTracker.captureElement, + eventParams.moveName, + eventParams.moveHandler, + true + ); } + + updatePointerCaptured( tracker, gPoint, true ); } @@ -1463,76 +1620,102 @@ * @private * @inner */ - function releasePointer( tracker, pointerType, pointerCount ) { - var pointsList = tracker.getActivePointersListByType( pointerType ), - eventParams; + function releasePointer( tracker, gPoint ) { + var eventParams; + var pointsList; + var cachedGPoint; - pointsList.captureCount -= (pointerCount || 1); - - if ( pointsList.captureCount === 0 ) { - if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { - tracker.element.releaseCapture(); - } else { - eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : pointerType ); - // We emulate mouse capture by hanging listeners on the document object. - // (Note we listen on the capture phase so the captured handlers will get called first) - // eslint-disable-next-line no-use-before-define - if (isInIframe && canAccessEvents(window.top)) { - $.removeEvent( - window.top, - eventParams.upName, - eventParams.upHandler, - true - ); + if ( $.MouseTracker.havePointerCapture ) { + if ( $.MouseTracker.havePointerEvents ) { + pointsList = tracker.getActivePointersListByType( gPoint.type ); + cachedGPoint = pointsList.getById( gPoint.id ); + if ( !cachedGPoint || !cachedGPoint.captured ) { + return; } + // Can throw InvalidPointerId + // (should never happen, but it does on Firefox 79 touch so we won't log a warning) + try { + tracker.element.releasePointerCapture( gPoint.id ); + //$.console.log('element.releasePointerCapture() called'); + } catch ( e ) { + //$.console.warn('releasePointerCapture() called on invalid pointer ID'); + } + } else { + tracker.element.releaseCapture(); + //$.console.log('element.releaseCapture() called'); + } + } else { + // Emulate mouse capture by hanging listeners on the document object. + // (Note we listen on the capture phase so the captured handlers will get called first) + //$.console.log('Emulated mouse capture release'); + eventParams = getCaptureEventParams( tracker, $.MouseTracker.havePointerEvents ? 'pointerevent' : gPoint.type ); + // https://github.com/openseadragon/openseadragon/pull/790 + if (isInIframe && canAccessEvents(window.top)) { $.removeEvent( - $.MouseTracker.captureElement, - eventParams.moveName, - eventParams.moveHandler, - true - ); - $.removeEvent( - $.MouseTracker.captureElement, + window.top, eventParams.upName, eventParams.upHandler, true ); } + $.removeEvent( + $.MouseTracker.captureElement, + eventParams.moveName, + eventParams.moveHandler, + true + ); + $.removeEvent( + $.MouseTracker.captureElement, + eventParams.upName, + eventParams.upHandler, + true + ); } + + updatePointerCaptured( tracker, gPoint, false ); + } + + + /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * @private + * @inner + */ + function getPointerId( event ) { + return ( $.MouseTracker.havePointerEvents ) ? event.pointerId : $.MouseTracker.mousePointerId; } /** * 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. + * + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) * @private * @inner */ function getPointerType( event ) { - var pointerTypeStr; - if ( $.MouseTracker.unprefixedPointerEvents ) { - pointerTypeStr = event.pointerType; + if ( $.MouseTracker.havePointerEvents ) { + // Note: IE pointer events bug - sends invalid pointerType on lostpointercapture events + // and possibly other events. We rely on sane, valid property values in DOM events, so for + // IE, when the pointerType is missing, we'll default to 'mouse'...should be right most of the time + return event.pointerType || (( $.Browser.vendor === $.BROWSERS.IE ) ? 'mouse' : ''); } 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 'mouse'; } - return pointerTypeStr; + } + + + /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * @private + * @inner + */ + function getIsPrimary( event ) { + return ( $.MouseTracker.havePointerEvents ) ? event.isPrimary : true; } @@ -1579,9 +1762,22 @@ * @inner */ function onClick( tracker, event ) { - if ( tracker.clickHandler ) { + //$.console.log('onClick ' + (tracker.userData ? tracker.userData.toString() : '')); + + var eventInfo = { + originalEvent: event, + eventType: 'click', + pointerType: 'mouse', + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { $.cancelEvent( event ); } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } } @@ -1590,9 +1786,22 @@ * @inner */ function onDblClick( tracker, event ) { - if ( tracker.dblClickHandler ) { + //$.console.log('onDblClick ' + (tracker.userData ? tracker.userData.toString() : '')); + + var eventInfo = { + originalEvent: event, + eventType: 'dblclick', + pointerType: 'mouse', + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { $.cancelEvent( event ); } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } } @@ -1604,7 +1813,6 @@ //$.console.log( "keydown %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey ); var propagate; if ( tracker.keyDownHandler ) { - event = $.getEvent( event ); propagate = tracker.keyDownHandler( { eventSource: tracker, @@ -1633,7 +1841,6 @@ //$.console.log( "keyup %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey ); var propagate; if ( tracker.keyUpHandler ) { - event = $.getEvent( event ); propagate = tracker.keyUpHandler( { eventSource: tracker, @@ -1662,7 +1869,6 @@ //$.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, @@ -1691,7 +1897,6 @@ //console.log( "focus %s", event ); var propagate; if ( tracker.focusHandler ) { - event = $.getEvent( event ); propagate = tracker.focusHandler( { eventSource: tracker, @@ -1715,7 +1920,6 @@ //console.log( "blur %s", event ); var propagate; if ( tracker.blurHandler ) { - event = $.getEvent( event ); propagate = tracker.blurHandler( { eventSource: tracker, @@ -1731,6 +1935,42 @@ } + /** + * @private + * @inner + */ + function onContextMenu( tracker, event ) { + //$.console.log('contextmenu ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); + + var eventInfo = { + originalEvent: event, + eventType: 'contextmenu', + pointerType: 'mouse', + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + // ContextMenu + if ( tracker.contextMenuHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) { + tracker.contextMenuHandler( + { + eventSource: tracker, + position: getPointRelativeToAbsolute( getMouseAbsolute( event ), tracker.element ), + originalEvent: eventInfo.originalEvent, + userData: tracker.userData + } + ); + } + + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( event ); + } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } + } + + /** * Handler for 'wheel' events * @@ -1749,8 +1989,6 @@ * @inner */ function onMouseWheel( tracker, event ) { - event = $.getEvent( event ); - // Simulate a 'wheel' event var simulatedEvent = { target: event.target || event.srcElement, @@ -1785,7 +2023,7 @@ */ function handleWheelEvent( tracker, event, originalEvent ) { var nDelta = 0, - propagate; + eventInfo; // The nDelta variable is gated to provide smooth z-index scrolling // since the mouse wheel allows for substantial deltas meant for rapid @@ -1794,8 +2032,18 @@ // TODO: Deltas in pixel mode should be accumulated then a scroll value computed after $.DEFAULT_SETTINGS.pixelsPerWheelLine threshold reached nDelta = event.deltaY < 0 ? 1 : -1; - if ( tracker.scrollHandler ) { - propagate = tracker.scrollHandler( + eventInfo = { + originalEvent: event, + eventType: 'wheel', + pointerType: 'mouse', + isEmulated: event !== originalEvent + }; + preProcessEvent( tracker, eventInfo ); + + if ( tracker.scrollHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) { + eventInfo.preventDefault = true; + + tracker.scrollHandler( { eventSource: tracker, pointerType: 'mouse', @@ -1808,283 +2056,42 @@ userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( originalEvent ); - } - } - } - - - /** - * @private - * @inner - */ - function isParentChild( parent, child ) - { - if ( parent === child ) { - return false; - } - while ( child && child !== parent ) { - child = child.parentNode; - } - return child === parent; - } - - - /** - * Only used on IE 8 - * - * @private - * @inner - */ - function onMouseEnter( tracker, event ) { - event = $.getEvent( event ); - - handleMouseEnter( tracker, event ); - } - - - /** - * @private - * @inner - */ - function onMouseOver( tracker, event ) { - event = $.getEvent( event ); - - if ( event.currentTarget === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { - return; } - handleMouseEnter( tracker, event ); - } + if ( eventInfo.stopPropagation ) { + $.stopEvent( originalEvent ); + } + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( originalEvent ); + } +} /** + * TODO Never actually seen this event fired, and documentation is tough to find * @private * @inner */ - function handleMouseEnter( tracker, event ) { + function onLoseCapture( tracker, event ) { + //$.console.log('losecapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); + var gPoint = { id: $.MouseTracker.mousePointerId, - type: 'mouse', - isPrimary: true, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() + type: 'mouse' }; - updatePointersEnter( tracker, event, [ gPoint ] ); - } - - - /** - * Only used on IE 8 - * - * @private - * @inner - */ - function onMouseLeave( tracker, event ) { - event = $.getEvent( event ); - - handleMouseExit( tracker, event ); - } - - - /** - * @private - * @inner - */ - function onMouseOut( tracker, event ) { - event = $.getEvent( event ); - - if ( event.currentTarget === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { - return; - } - - handleMouseExit( tracker, event ); - } - - - /** - * @private - * @inner - */ - function handleMouseExit( tracker, event ) { - var gPoint = { - id: $.MouseTracker.mousePointerId, - type: 'mouse', - isPrimary: true, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() + var eventInfo = { + originalEvent: event, + eventType: 'lostpointercapture', + pointerType: 'mouse', + isEmulated: false }; + preProcessEvent( tracker, eventInfo ); - updatePointersExit( tracker, event, [ gPoint ] ); - } + updatePointerCaptured( tracker, gPoint, false ); - - /** - * Returns a W3C DOM level 3 standard button value given an event.button property: - * -1 == none, 0 == primary/left, 1 == middle, 2 == secondary/right, 3 == X1/back, 4 == X2/forward, 5 == eraser (pen) - * @private - * @inner - */ - function getStandardizedButton( button ) { - if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { - // On IE 8, 0 == none, 1 == left, 2 == right, 3 == left and right, 4 == middle, 5 == left and middle, 6 == right and middle, 7 == all three - // TODO: Support chorded (multiple) button presses on IE 8? - if ( button === 1 ) { - return 0; - } else if ( button === 2 ) { - return 2; - } else if ( button === 4 ) { - return 1; - } else { - return -1; - } - } else { - return button; - } - } - - - /** - * @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 ], getStandardizedButton( event.button ) ) ) { + if ( eventInfo.stopPropagation ) { $.stopEvent( event ); - capturePointer( tracker, 'mouse' ); - } - - if ( tracker.clickHandler || tracker.dblClickHandler || tracker.pressHandler || tracker.dragHandler || tracker.dragEndHandler ) { - $.cancelEvent( event ); - } - } - - - /** - * @private - * @inner - */ - function onMouseUp( tracker, event ) { - handleMouseUp( tracker, event ); - } - - /** - * This handler is attached to the window object (on the capture phase) to emulate mouse capture. - * onMouseUp is still attached to the tracked element, so stop propagation to avoid processing twice. - * - * @private - * @inner - */ - function onMouseUpCaptured( tracker, event ) { - handleMouseUp( tracker, event ); - $.stopEvent( event ); - } - - - /** - * @private - * @inner - */ - function handleMouseUp( tracker, event ) { - var gPoint; - - event = $.getEvent( event ); - - gPoint = { - id: $.MouseTracker.mousePointerId, - type: 'mouse', - isPrimary: true, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - if ( updatePointersUp( tracker, event, [ gPoint ], getStandardizedButton( event.button ) ) ) { - releasePointer( tracker, 'mouse' ); - } - } - - - /** - * @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. - * 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 abortContacts( tracker, event, pointsList ) { - var i, - gPointCount = pointsList.getLength(), - abortGPoints = []; - - // Check contact count for hoverable pointer types before aborting - if (pointsList.type === 'touch' || pointsList.contacts > 0) { - for ( i = 0; i < gPointCount; i++ ) { - abortGPoints.push( pointsList.getByIndex( i ) ); - } - - if ( abortGPoints.length > 0 ) { - // simulate touchend/mouseup - updatePointersUp( tracker, event, abortGPoints, 0 ); // 0 means primary button press/release or touch contact - // release pointer capture - pointsList.captureCount = 1; - releasePointer( tracker, pointsList.type ); - // simulate touchleave/mouseout - updatePointersExit( tracker, event, abortGPoints ); - } } } @@ -2096,55 +2103,50 @@ function onTouchStart( tracker, event ) { var time, i, - j, touchCount = event.changedTouches.length, - gPoints = [], - parentGPoints, + gPoint, pointsList = tracker.getActivePointersListByType( 'touch' ); time = $.now(); + //$.console.log('touchstart ' + (tracker.userData ? tracker.userData.toString() : '')); + if ( pointsList.getLength() > event.touches.length - touchCount ) { - $.console.warn('Tracked touch contact count doesn\'t match event.touches.length. Removing all tracked touch pointers.'); - abortContacts( tracker, event, pointsList ); + $.console.warn('Tracked touch contact count doesn\'t match event.touches.length'); } + var eventInfo = { + originalEvent: event, + eventType: 'pointerdown', + pointerType: 'touch', + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { + gPoint = { id: event.changedTouches[ i ].identifier, type: 'touch', - // isPrimary not set - let the updatePointers functions determine it + // Simulate isPrimary + isPrimary: pointsList.getLength() === 0, currentPos: getMouseAbsolute( event.changedTouches[ i ] ), currentTime: time - } ); + }; + + // simulate touchenter on our tracked element + updatePointerEnter( tracker, eventInfo, gPoint ); + + updatePointerCaptured( tracker, gPoint, true ); + + updatePointerDown( tracker, eventInfo, gPoint, 0 ); } - // simulate touchenter on our tracked element - updatePointersEnter( tracker, event, gPoints ); - - // simulate touchenter on our tracked element's tracked ancestor elements - for ( i = 0; i < MOUSETRACKERS.length; i++ ) { - if ( MOUSETRACKERS[ i ] !== tracker && MOUSETRACKERS[ i ].isTracking() && isParentChild( MOUSETRACKERS[ i ].element, tracker.element ) ) { - parentGPoints = []; - for ( j = 0; j < touchCount; j++ ) { - parentGPoints.push( { - id: event.changedTouches[ j ].identifier, - type: 'touch', - // isPrimary not set - let the updatePointers functions determine it - currentPos: getMouseAbsolute( event.changedTouches[ j ] ), - currentTime: time - } ); - } - updatePointersEnter( MOUSETRACKERS[ i ], event, parentGPoints ); - } + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( event ); } - - if ( updatePointersDown( tracker, event, gPoints, 0 ) ) { // 0 means primary button press/release or touch contact + if ( eventInfo.stopPropagation ) { $.stopEvent( event ); - capturePointer( tracker, 'touch', touchCount ); } - - $.cancelEvent( event ); } @@ -2153,72 +2155,45 @@ * @inner */ function onTouchEnd( tracker, event ) { - handleTouchEnd( tracker, event ); - } - - - /** - * This handler is attached to the window object (on the capture phase) to emulate pointer capture. - * onTouchEnd is still attached to the tracked element, so stop propagation to avoid processing twice. - * - * @private - * @inner - */ - function onTouchEndCaptured( tracker, event ) { - handleTouchEnd( tracker, event ); - $.stopEvent( event ); - } - - - /** - * @private - * @inner - */ - function handleTouchEnd( tracker, event ) { var time, i, - j, touchCount = event.changedTouches.length, - gPoints = [], - parentGPoints; + gPoint; time = $.now(); + //$.console.log('touchend ' + (tracker.userData ? tracker.userData.toString() : '')); + + var eventInfo = { + originalEvent: event, + eventType: 'pointerup', + pointerType: 'touch', + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { + gPoint = { id: event.changedTouches[ i ].identifier, type: 'touch', - // isPrimary not set - let the updatePointers functions determine it currentPos: getMouseAbsolute( event.changedTouches[ i ] ), currentTime: time - } ); + }; + + updatePointerUp( tracker, eventInfo, gPoint, 0 ); + + updatePointerCaptured( tracker, gPoint, false ); + + // simulate touchleave on our tracked element + updatePointerLeave( tracker, eventInfo, gPoint ); } - if ( updatePointersUp( tracker, event, gPoints, 0 ) ) { - releasePointer( tracker, 'touch', touchCount ); + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( event ); } - - // simulate touchleave on our tracked element - updatePointersExit( tracker, event, gPoints ); - - // simulate touchleave on our tracked element's tracked ancestor elements - for ( i = 0; i < MOUSETRACKERS.length; i++ ) { - if ( MOUSETRACKERS[ i ] !== tracker && MOUSETRACKERS[ i ].isTracking() && isParentChild( MOUSETRACKERS[ i ].element, tracker.element ) ) { - parentGPoints = []; - for ( j = 0; j < touchCount; j++ ) { - parentGPoints.push( { - id: event.changedTouches[ j ].identifier, - type: 'touch', - // isPrimary not set - let the updatePointers functions determine it - currentPos: getMouseAbsolute( event.changedTouches[ j ] ), - currentTime: time - } ); - } - updatePointersExit( MOUSETRACKERS[ i ], event, parentGPoints ); - } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); } - - $.cancelEvent( event ); } @@ -2227,45 +2202,38 @@ * @inner */ function onTouchMove( tracker, event ) { - handleTouchMove( tracker, event ); - } - - - /** - * This handler is attached to the window object (on the capture phase) to emulate pointer capture. - * onTouchMove is still attached to the tracked element, so stop propagation to avoid processing twice. - * - * @private - * @inner - */ - function onTouchMoveCaptured( tracker, event ) { - handleTouchMove( tracker, event ); - $.stopEvent( event ); - } - - - /** - * @private - * @inner - */ - function handleTouchMove( tracker, event ) { - var i, + var time, + i, touchCount = event.changedTouches.length, - gPoints = []; + gPoint; + + time = $.now(); + + var eventInfo = { + originalEvent: event, + eventType: 'pointermove', + pointerType: 'touch', + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { + gPoint = { id: event.changedTouches[ i ].identifier, type: 'touch', - // isPrimary not set - let the updatePointers functions determine it currentPos: getMouseAbsolute( event.changedTouches[ i ] ), - currentTime: $.now() - } ); + currentTime: time + }; + + updatePointerMove( tracker, eventInfo, gPoint ); } - updatePointersMove( tracker, event, gPoints ); - - $.cancelEvent( event ); + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( event ); + } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } } @@ -2274,9 +2242,33 @@ * @inner */ function onTouchCancel( tracker, event ) { - var pointsList = tracker.getActivePointersListByType('touch'); + var touchCount = event.changedTouches.length, + i, + gPoint; - abortContacts( tracker, event, pointsList ); + //$.console.log('touchcancel ' + (tracker.userData ? tracker.userData.toString() : '')); + + var eventInfo = { + originalEvent: event, + eventType: 'pointercancel', + pointerType: 'touch', + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + for ( i = 0; i < touchCount; i++ ) { + gPoint = { + id: event.changedTouches[ i ].identifier, + type: 'touch' + }; + + //TODO need to only do this if our element is target? + updatePointerCancel( tracker, eventInfo, gPoint ); + } + + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } } @@ -2285,8 +2277,9 @@ * @inner */ function onGestureStart( tracker, event ) { - event.stopPropagation(); - event.preventDefault(); + if ( !$.eventIsCanceled( event ) ) { + event.preventDefault(); + } return false; } @@ -2296,8 +2289,9 @@ * @inner */ function onGestureChange( tracker, event ) { - event.stopPropagation(); - event.preventDefault(); + if ( !$.eventIsCanceled( event ) ) { + event.preventDefault(); + } return false; } @@ -2306,22 +2300,28 @@ * @private * @inner */ - function onPointerOver( tracker, event ) { - var gPoint; + function onGotPointerCapture( tracker, event ) { + //$.console.log('gotpointercapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); - if ( event.currentTarget === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { - return; + var eventInfo = { + originalEvent: event, + eventType: 'gotpointercapture', + pointerType: getPointerType( event ), + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + if ( event.target === tracker.element ) { + //$.console.log('gotpointercapture ' + (tracker.userData ? tracker.userData.toString() : '')); + updatePointerCaptured( tracker, { + id: event.pointerId, + type: getPointerType( event ) + }, true ); } - gPoint = { - id: event.pointerId, - type: getPointerType( event ), - isPrimary: event.isPrimary, - currentPos: getMouseAbsolute( event ), - currentTime: $.now() - }; - - updatePointersEnter( tracker, event, [ gPoint ] ); + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } } @@ -2329,52 +2329,233 @@ * @private * @inner */ + function onLostPointerCapture( tracker, event ) { + //$.console.log('lostpointercapture ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); + + var eventInfo = { + originalEvent: event, + eventType: 'lostpointercapture', + pointerType: getPointerType( event ), + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + if ( event.target === tracker.element ) { + //$.console.log('lostpointercapture ' + (tracker.userData ? tracker.userData.toString() : '')); + updatePointerCaptured( tracker, { + id: event.pointerId, + type: getPointerType( event ) + }, false ); + } + + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } + } + + + /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * + * @private + * @inner + */ + function onPointerEnter( tracker, event ) { + //$.console.log('pointerenter ' + (tracker.userData ? tracker.userData.toString() : '')); + + var gPoint = { + id: getPointerId( event ), + type: getPointerType( event ), + isPrimary: getIsPrimary( event ), + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + // pointerenter doesn't bubble and is not cancelable, but we call + // preProcessEvent() so it's dispatched to preProcessEventHandler + // if necessary + var eventInfo = { + originalEvent: event, + eventType: 'pointerenter', + pointerType: gPoint.type, + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + updatePointerEnter( tracker, eventInfo, gPoint ); + } + + + /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * + * @private + * @inner + */ + function onPointerLeave( tracker, event ) { + //$.console.log('pointerleave ' + (tracker.userData ? tracker.userData.toString() : '')); + + var gPoint = { + id: getPointerId( event ), + type: getPointerType( event ), + isPrimary: getIsPrimary( event ), + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + // pointerleave doesn't bubble and is not cancelable, but we call + // preProcessEvent() so it's dispatched to preProcessEventHandler + // if necessary + var eventInfo = { + originalEvent: event, + eventType: 'pointerleave', + pointerType: gPoint.type, + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + updatePointerLeave( tracker, eventInfo, gPoint ); + } + + + /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * + * @private + * @inner + */ + function onPointerOver( tracker, event ) { + //$.console.log('pointerover ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); + + var gPoint = { + id: getPointerId( event ), + type: getPointerType( event ), + isPrimary: getIsPrimary( event ), + currentPos: getMouseAbsolute( event ), + currentTime: $.now() + }; + + var eventInfo = { + originalEvent: event, + eventType: 'pointerover', + pointerType: gPoint.type, + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + updatePointerOver( tracker, eventInfo, gPoint ); + + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( event ); + } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } + } + + + /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * + * @private + * @inner + */ function onPointerOut( tracker, event ) { - var gPoint; + //$.console.log('pointerout ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); - if ( event.currentTarget === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { - return; - } - - gPoint = { - id: event.pointerId, + var gPoint = { + id: getPointerId( event ), type: getPointerType( event ), - isPrimary: event.isPrimary, + isPrimary: getIsPrimary( event ), currentPos: getMouseAbsolute( event ), currentTime: $.now() }; - updatePointersExit( tracker, event, [ gPoint ] ); + var eventInfo = { + originalEvent: event, + eventType: 'pointerout', + pointerType: gPoint.type, + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + updatePointerOut( tracker, eventInfo, gPoint ); + + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( event ); + } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } } /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * * @private * @inner */ function onPointerDown( tracker, event ) { - var gPoint; + //$.console.log('onPointerDown ' + (tracker.userData ? tracker.userData.toString() : '')); + // $.console.log('onPointerDown ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + event.target.tagName); - gPoint = { - id: event.pointerId, + // Most browsers implicitly capture touch pointer events + // Note no IE versions have element.hasPointerCapture() so no implicit + // pointer capture possible + var implicitlyCaptured = ($.MouseTracker.havePointerEvents && + tracker.element.hasPointerCapture && + $.Browser.vendor !== $.BROWSERS.IE) ? + tracker.element.hasPointerCapture(event.pointerId) : false; + // if (implicitlyCaptured) { + // $.console.log('pointerdown implicitlyCaptured ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); + // } else { + // $.console.log('pointerdown not implicitlyCaptured ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); + // } + + var gPoint = { + id: getPointerId( event ), type: getPointerType( event ), - isPrimary: event.isPrimary, + isPrimary: getIsPrimary( event ), currentPos: getMouseAbsolute( event ), currentTime: $.now() }; - if ( updatePointersDown( tracker, event, [ gPoint ], event.button ) ) { - $.stopEvent( event ); - capturePointer( tracker, gPoint.type ); - } + var eventInfo = { + originalEvent: event, + eventType: 'pointerdown', + pointerType: gPoint.type, + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); - if ( tracker.clickHandler || tracker.dblClickHandler || tracker.pressHandler || tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { + updatePointerDown( tracker, eventInfo, gPoint, event.button ); + + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { $.cancelEvent( event ); } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } + if ( eventInfo.shouldCapture && !implicitlyCaptured ) { + //$.console.log('pointerdown calling capturePointer() ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); + capturePointer( tracker, gPoint ); + } else if ( !eventInfo.shouldCapture && implicitlyCaptured ) { + //$.console.log('pointerdown calling releasePointer() ' + (tracker.userData ? tracker.userData.toString() : '') + ' ' + (event.target === tracker.element ? 'tracker.element' : '')); + releasePointer( tracker, gPoint ); //TODO should we do this? Investigate when implementing bubble handling + } } /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * * @private * @inner */ @@ -2384,6 +2565,9 @@ /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * * This handler is attached to the window object (on the capture phase) to emulate mouse capture. * onPointerUp is still attached to the tracked element, so stop propagation to avoid processing twice. * @@ -2400,27 +2584,56 @@ /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * * @private * @inner */ function handlePointerUp( tracker, event ) { var gPoint; + //$.console.log('onPointerUp ' + (tracker.userData ? tracker.userData.toString() : '')); + gPoint = { - id: event.pointerId, + id: getPointerId( event ), type: getPointerType( event ), - isPrimary: event.isPrimary, + isPrimary: getIsPrimary( event ), currentPos: getMouseAbsolute( event ), currentTime: $.now() }; - if ( updatePointersUp( tracker, event, [ gPoint ], event.button ) ) { - releasePointer( tracker, gPoint.type ); + var eventInfo = { + originalEvent: event, + eventType: 'pointerup', + pointerType: gPoint.type, + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + updatePointerUp( tracker, eventInfo, gPoint, event.button ); + + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( event ); + } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } + + // Per spec, pointerup events are supposed to release capture. Not all browser + // versions have adhered to the spec, and there's no harm in releasing + // explicitly + if ( eventInfo.shouldReleaseCapture && event.target === tracker.element ) { + //$.stopEvent( event ); + releasePointer( tracker, gPoint ); } } /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * * @private * @inner */ @@ -2430,6 +2643,9 @@ /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * * This handler is attached to the window object (on the capture phase) to emulate mouse capture. * onPointerMove is still attached to the tracked element, so stop propagation to avoid processing twice. * @@ -2446,22 +2662,39 @@ /** + * Note: Called for both pointer events and legacy mouse events + * ($.MouseTracker.havePointerEvents determines which) + * * @private * @inner */ function handlePointerMove( tracker, event ) { // Pointer changed coordinates, button state, pressure, tilt, or contact geometry (e.g. width and height) - var gPoint; - gPoint = { - id: event.pointerId, + var gPoint = { + id: getPointerId( event ), type: getPointerType( event ), - isPrimary: event.isPrimary, + isPrimary: getIsPrimary( event ), currentPos: getMouseAbsolute( event ), currentTime: $.now() }; - updatePointersMove( tracker, event, [ gPoint ] ); + var eventInfo = { + originalEvent: event, + eventType: 'pointermove', + pointerType: gPoint.type, + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + updatePointerMove( tracker, eventInfo, gPoint ); + + if ( eventInfo.preventDefault && !eventInfo.defaultPrevented ) { + $.cancelEvent( event ); + } + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } } @@ -2470,14 +2703,27 @@ * @inner */ function onPointerCancel( tracker, event ) { - var gPoint; + //$.console.log('pointercancel ' + (tracker.userData ? tracker.userData.toString() : '')); - gPoint = { + var gPoint = { id: event.pointerId, type: getPointerType( event ) }; - updatePointersCancel( tracker, event, [ gPoint ] ); + var eventInfo = { + originalEvent: event, + eventType: 'pointercancel', + pointerType: gPoint.type, + isEmulated: false + }; + preProcessEvent( tracker, eventInfo ); + + //TODO need to only do this if our element is target? + updatePointerCancel( tracker, eventInfo, gPoint ); + + if ( eventInfo.stopPropagation ) { + $.stopEvent( event ); + } } @@ -2496,16 +2742,6 @@ * @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 ( !Object.prototype.hasOwnProperty.call( gPoint, 'isPrimary' ) ) { - if ( pointsList.getLength() === 0 ) { - gPoint.isPrimary = true; - } else { - gPoint.isPrimary = false; - } - } gPoint.speed = 0; gPoint.direction = 0; gPoint.contactPos = gPoint.currentPos; @@ -2521,29 +2757,26 @@ * @function * @private * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. * @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; + function stopTrackingPointer( tracker, pointsList, gPoint ) { + var listLength; - if ( pointsList.getById( gPoint.id ) ) { - listLength = pointsList.removeById( gPoint.id ); + var trackedGPoint = pointsList.getById( 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 ( !Object.prototype.hasOwnProperty.call( gPoint, 'isPrimary' ) ) { - primaryPoint = pointsList.getPrimary(); - if ( !primaryPoint ) { - primaryPoint = pointsList.getByIndex( 0 ); - if ( primaryPoint ) { - primaryPoint.isPrimary = true; - } - } + if ( trackedGPoint ) { + if ( trackedGPoint.captured ) { + releasePointer( tracker, trackedGPoint ); + pointsList.removeContact(); } + + listLength = pointsList.removeById( gPoint.id ); } else { listLength = pointsList.getLength(); } @@ -2553,129 +2786,273 @@ /** + * @function + * @private + * @inner + */ + function getEventProcessDefaults( tracker, eventInfo ) { + switch ( eventInfo.eventType ) { + case 'pointermove': + eventInfo.isStopable = true; + eventInfo.isCancelable = true; + eventInfo.preventDefault = false; + eventInfo.preventGesture = !tracker.hasGestureHandlers; + eventInfo.stopPropagation = false; + break; + case 'pointerover': + case 'pointerout': + eventInfo.isStopable = true; + eventInfo.isCancelable = true; + eventInfo.preventDefault = false; + eventInfo.preventGesture = false; + eventInfo.stopPropagation = false; + break; + case 'pointerdown': + eventInfo.isStopable = true; + eventInfo.isCancelable = true; + eventInfo.preventDefault = false;//tracker.hasGestureHandlers; + eventInfo.preventGesture = !tracker.hasGestureHandlers; + eventInfo.stopPropagation = false; + break; + case 'pointerup': + eventInfo.isStopable = true; + eventInfo.isCancelable = true; + eventInfo.preventDefault = false; + eventInfo.preventGesture = !tracker.hasGestureHandlers; + eventInfo.stopPropagation = false; + break; + case 'wheel': + eventInfo.isStopable = true; + eventInfo.isCancelable = true; + eventInfo.preventDefault = false;//tracker.hasScrollHandler; + eventInfo.preventGesture = !tracker.hasScrollHandler; + eventInfo.stopPropagation = false; + break; + case 'gotpointercapture': + case 'lostpointercapture': + case 'pointercancel': + eventInfo.isStopable = true; + eventInfo.isCancelable = false; + eventInfo.preventDefault = false; + eventInfo.preventGesture = false; + eventInfo.stopPropagation = false; + break; + case 'click': + eventInfo.isStopable = true; + eventInfo.isCancelable = true; + eventInfo.preventDefault = !!tracker.clickHandler; + eventInfo.preventGesture = false; + eventInfo.stopPropagation = false; + break; + case 'dblclick': + eventInfo.isStopable = true; + eventInfo.isCancelable = true; + eventInfo.preventDefault = !!tracker.dblClickHandler; + eventInfo.preventGesture = false; + eventInfo.stopPropagation = false; + break; + case 'contextmenu': + eventInfo.isStopable = true; + eventInfo.isCancelable = true; + eventInfo.preventDefault = false;//tracker.hasContextMenuHandler; + eventInfo.preventGesture = true;//!tracker.hasContextMenuHandler; + eventInfo.stopPropagation = false; + break; + case 'pointerenter': + case 'pointerleave': + default: + eventInfo.isStopable = false; + eventInfo.isCancelable = false; + eventInfo.preventDefault = false; + eventInfo.preventGesture = false; + eventInfo.stopPropagation = false; + break; + } + } + + + /** + * Sets up for and calls preProcessEventHandler. Call with the following parameters - + * this function will fill in the rest of the preProcessEventHandler event object + * properties + * * @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 {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * @param {Object} eventInfo.originalEvent + * @param {String} eventInfo.eventType + * @param {String} eventInfo.pointerType + * @param {Boolean} eventInfo.isEmulated */ - function updatePointersEnter( tracker, event, gPoints ) { - var pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), - i, - gPointCount = gPoints.length, - curGPoint, + function preProcessEvent( tracker, eventInfo ) { + eventInfo.eventSource = tracker; + eventInfo.eventPhase = eventInfo.originalEvent ? + ((typeof eventInfo.originalEvent.eventPhase !== 'undefined') ? + eventInfo.originalEvent.eventPhase : 0) : 0; + eventInfo.defaultPrevented = $.eventIsCanceled( eventInfo.originalEvent ); + eventInfo.shouldCapture = false; + eventInfo.shouldReleaseCapture = false; + eventInfo.userData = tracker.userData; + + getEventProcessDefaults( tracker, eventInfo ); + + if ( tracker.preProcessEventHandler ) { + tracker.preProcessEventHandler( eventInfo ); + } + } + + + /** + * Sets or resets the captured property on the tracked pointer matching the passed gPoint's id/type + * + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {Object} gPoint + * An object with id and type properties describing the pointer to update. + * @param {Boolean} isCaptured + * Value to set the captured property to. + */ + function updatePointerCaptured( tracker, gPoint, isCaptured ) { + var pointsList = tracker.getActivePointersListByType( gPoint.type ); + var updateGPoint = pointsList.getById( gPoint.id ); + + if ( updateGPoint ) { + if ( isCaptured && !updateGPoint.captured ) { + updateGPoint.captured = true; + pointsList.captureCount++; + } else if ( !isCaptured && updateGPoint.captured ) { + updateGPoint.captured = false; + pointsList.captureCount--; + if ( pointsList.captureCount < 0 ) { + pointsList.captureCount = 0; + $.console.warn('updatePointerCaptured() - pointsList.captureCount went negative'); + } + } + } else { + $.console.warn('updatePointerCaptured() called on untracked pointer'); + } + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * Processing info for originating DOM event. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint + * Gesture point associated with the event. + */ + function updatePointerEnter( tracker, eventInfo, gPoint ) { + var pointsList = tracker.getActivePointersListByType( gPoint.type ), + updateGPoint; + + updateGPoint = pointsList.getById( gPoint.id ); + + if ( updateGPoint ) { + // Already tracking the pointer...update it + updateGPoint.insideElement = true; + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = gPoint.currentPos; + updateGPoint.currentTime = gPoint.currentTime; + + gPoint = updateGPoint; + } else { + // Initialize for tracking and add to the tracking list + gPoint.captured = false; // Handled by updatePointerCaptured() + gPoint.insideElementPressed = false; + gPoint.insideElement = true; + startTrackingPointer( pointsList, gPoint ); + } + + // Enter (doesn't bubble and not cancelable) + if ( tracker.enterHandler ) { + tracker.enterHandler( + { + eventSource: tracker, + pointerType: gPoint.type, + position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ), + buttons: pointsList.buttons, + pointers: tracker.getActivePointerCount(), + insideElementPressed: gPoint.insideElementPressed, + buttonDownAny: pointsList.buttons !== 0, + isTouchEvent: gPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, + userData: tracker.userData + } + ); + } + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * Processing info for originating DOM event. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint + * Gesture point associated with the event. + */ + function updatePointerLeave( tracker, eventInfo, gPoint ) { + var pointsList = tracker.getActivePointersListByType(gPoint.type), updateGPoint, - propagate; + dispatchEventObj; - for ( i = 0; i < gPointCount; i++ ) { - curGPoint = gPoints[ i ]; - updateGPoint = pointsList.getById( curGPoint.id ); + updateGPoint = pointsList.getById( gPoint.id ); - if ( updateGPoint ) { - // Already tracking the pointer...update it - updateGPoint.insideElement = true; + 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; - - curGPoint = updateGPoint; + updateGPoint.currentPos = gPoint.currentPos; + updateGPoint.currentTime = gPoint.currentTime; } else { - // Initialize for tracking and add to the tracking list - curGPoint.captured = false; - curGPoint.insideElementPressed = false; - curGPoint.insideElement = true; - startTrackingPointer( pointsList, curGPoint ); + stopTrackingPointer( tracker, pointsList, updateGPoint ); } - // Enter - if ( tracker.enterHandler ) { - propagate = tracker.enterHandler( - { - eventSource: tracker, - pointerType: curGPoint.type, - position: getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), - buttons: pointsList.buttons, - pointers: tracker.getActivePointerCount(), - insideElementPressed: curGPoint.insideElementPressed, - buttonDownAny: pointsList.buttons !== 0, - isTouchEvent: curGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } + gPoint = updateGPoint; + } else { + gPoint.captured = false; // Handled by updatePointerCaptured() + gPoint.insideElementPressed = false; } - } + // Leave (doesn't bubble and not cancelable) + // Note: exitHandler is deprecated (v2.5.0), replaced by leaveHandler + if ( tracker.leaveHandler || tracker.exitHandler ) { + dispatchEventObj = { + eventSource: tracker, + pointerType: gPoint.type, + // GitHub PR: https://github.com/openseadragon/openseadragon/pull/1754 (gPoint.currentPos && ) + position: gPoint.currentPos && getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ), + buttons: pointsList.buttons, + pointers: tracker.getActivePointerCount(), + insideElementPressed: gPoint.insideElementPressed, + buttonDownAny: pointsList.buttons !== 0, + isTouchEvent: gPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, + userData: tracker.userData + }; - /** - * @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 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; + if ( tracker.leaveHandler ) { + tracker.leaveHandler( dispatchEventObj ); } - - // Exit + // Deprecated if ( tracker.exitHandler ) { - propagate = tracker.exitHandler( - { - eventSource: tracker, - pointerType: curGPoint.type, - position: curGPoint.currentPos && getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), - buttons: pointsList.buttons, - pointers: tracker.getActivePointerCount(), - 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 ); - } + tracker.exitHandler( dispatchEventObj ); } } } @@ -2687,167 +3064,229 @@ * @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 {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * Processing info for originating DOM event. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint + * Gesture point associated with the event. + */ + function updatePointerOver( tracker, eventInfo, gPoint ) { + var pointsList, + updateGPoint; + + pointsList = tracker.getActivePointersListByType( gPoint.type ); + + updateGPoint = pointsList.getById( gPoint.id ); + + if ( updateGPoint ) { + gPoint = updateGPoint; + } else { + gPoint.captured = false; + gPoint.insideElementPressed = false; + //gPoint.insideElement = true; // Tracked by updatePointerEnter + } + + if ( tracker.overHandler ) { + // Over + tracker.overHandler( + { + eventSource: tracker, + pointerType: gPoint.type, + position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ), + buttons: pointsList.buttons, + pointers: tracker.getActivePointerCount(), + insideElementPressed: gPoint.insideElementPressed, + buttonDownAny: pointsList.buttons !== 0, + isTouchEvent: gPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, + preventDefaultAction: false, + userData: tracker.userData + } + ); + } + } + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * Processing info for originating DOM event. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint + * Gesture point associated with the event. + */ + function updatePointerOut( tracker, eventInfo, gPoint ) { + var pointsList, + updateGPoint; + + pointsList = tracker.getActivePointersListByType(gPoint.type); + + updateGPoint = pointsList.getById( gPoint.id ); + + if ( updateGPoint ) { + gPoint = updateGPoint; + } else { + gPoint.captured = false; + gPoint.insideElementPressed = false; + //gPoint.insideElement = true; // Tracked by updatePointerEnter + } + + if ( tracker.outHandler ) { + // Out + tracker.outHandler( { + eventSource: tracker, + pointerType: gPoint.type, + position: gPoint.currentPos && getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ), + buttons: pointsList.buttons, + pointers: tracker.getActivePointerCount(), + insideElementPressed: gPoint.insideElementPressed, + buttonDownAny: pointsList.buttons !== 0, + isTouchEvent: gPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, + preventDefaultAction: false, + userData: tracker.userData + } ); + } + } + + + /** + * @function + * @private + * @inner + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the MouseTracker instance. + * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * Processing info for originating DOM event. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint + * Gesture point associated with the event. * @param {Number} buttonChanged * The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 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 ) { + function updatePointerDown( tracker, eventInfo, gPoint, buttonChanged ) { var delegate = THIS[ tracker.hash ], - propagate, - pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), - i, - gPointCount = gPoints.length, - curGPoint, + pointsList = tracker.getActivePointersListByType( gPoint.type ), updateGPoint; - if ( typeof event.buttons !== 'undefined' ) { - pointsList.buttons = event.buttons; + if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) { + pointsList.buttons = eventInfo.originalEvent.buttons; } else { - if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { - 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; - } - } 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; - } + 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; } } - // Some pointers may steal control from another pointer without firing the appropriate release events - // e.g. Touching a screen while click-dragging with certain mice. - var otherPointsLists = tracker.getActivePointersListsExceptType(gPoints[ 0 ].type); - for (i = 0; i < otherPointsLists.length; i++) { - //If another pointer has contact, simulate the release - abortContacts(tracker, event, otherPointsLists[i]); // No-op if no active pointer - } - // Only capture and track primary button, pen, and touch contacts if ( buttonChanged !== 0 ) { + eventInfo.shouldCapture = false; + eventInfo.shouldReleaseCapture = false; + // Aux Press - if ( tracker.nonPrimaryPressHandler ) { - propagate = tracker.nonPrimaryPressHandler( + if ( tracker.nonPrimaryPressHandler && + !eventInfo.preventGesture && + !eventInfo.defaultPrevented ) { + eventInfo.preventDefault = true; + + tracker.nonPrimaryPressHandler( { eventSource: tracker, - pointerType: gPoints[ 0 ].type, - position: getPointRelativeToAbsolute( gPoints[ 0 ].currentPos, tracker.element ), + pointerType: gPoint.type, + position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ), button: buttonChanged, buttons: pointsList.buttons, - isTouchEvent: gPoints[ 0 ].type === 'touch', - originalEvent: event, + isTouchEvent: gPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, preventDefaultAction: false, userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( event ); - } } - return false; + return; } - for ( i = 0; i < gPointCount; i++ ) { - curGPoint = gPoints[ i ]; - updateGPoint = pointsList.getById( curGPoint.id ); + updateGPoint = pointsList.getById( gPoint.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; + if ( updateGPoint ) { + // Already tracking the pointer...update it + //updateGPoint.captured = true; // Handled by updatePointerCaptured() + updateGPoint.insideElementPressed = true; + updateGPoint.insideElement = true; + updateGPoint.contactPos = gPoint.currentPos; + updateGPoint.contactTime = gPoint.currentTime; + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = gPoint.currentPos; + updateGPoint.currentTime = gPoint.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 ); - } + gPoint = updateGPoint; + } else { + // Initialize for tracking and add to the tracking list (no pointerover or pointermove event occurred before this) + gPoint.captured = false; // Handled by updatePointerCaptured() + gPoint.insideElementPressed = true; + gPoint.insideElement = true; + startTrackingPointer( pointsList, gPoint ); + } + + if ( !eventInfo.preventGesture && !eventInfo.defaultPrevented ) { + eventInfo.shouldCapture = true; + eventInfo.shouldReleaseCapture = false; + eventInfo.preventDefault = true; pointsList.addContact(); //$.console.log('contacts++ ', pointsList.contacts); if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { - $.MouseTracker.gesturePointVelocityTracker.addPoint( tracker, curGPoint ); + $.MouseTracker.gesturePointVelocityTracker.addPoint( tracker, gPoint ); } if ( pointsList.contacts === 1 ) { // Press - if ( tracker.pressHandler ) { - propagate = tracker.pressHandler( + if ( tracker.pressHandler && !eventInfo.preventGesture ) { + tracker.pressHandler( { eventSource: tracker, - pointerType: curGPoint.type, - position: getPointRelativeToAbsolute( curGPoint.contactPos, tracker.element ), + pointerType: gPoint.type, + position: getPointRelativeToAbsolute( gPoint.contactPos, tracker.element ), buttons: pointsList.buttons, - isTouchEvent: curGPoint.type === 'touch', - originalEvent: event, + isTouchEvent: gPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, preventDefaultAction: false, userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( event ); - } } } else if ( pointsList.contacts === 2 ) { - if ( tracker.pinchHandler && curGPoint.type === 'touch' ) { + if ( tracker.pinchHandler && gPoint.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 ); } } + } else { + eventInfo.shouldCapture = false; + eventInfo.shouldReleaseCapture = false; } - - return true; } @@ -2857,262 +3296,126 @@ * @inner * @param {OpenSeadragon.MouseTracker} tracker * A reference to the MouseTracker instance. - * @param {Object} event - * A reference to the originating DOM event. - * @param {Array.} gPoints + * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * Processing info for originating DOM event. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint * Gesture points associated with the event. * @param {Number} buttonChanged * The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 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 ) { + function updatePointerUp( tracker, eventInfo, gPoint, buttonChanged ) { var delegate = THIS[ tracker.hash ], - pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), - propagate, + pointsList = tracker.getActivePointersListByType( gPoint.type ), releasePoint, releaseTime, - i, - gPointCount = gPoints.length, - curGPoint, updateGPoint, - releaseCapture = false, wasCaptured = false, quick; - if ( typeof event.buttons !== 'undefined' ) { - pointsList.buttons = event.buttons; + if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) { + pointsList.buttons = eventInfo.originalEvent.buttons; } else { - if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { - 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; - } - } 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; - } + 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; } } + eventInfo.shouldCapture = false; + // Only capture and track primary button, pen, and touch contacts if ( buttonChanged !== 0 ) { + eventInfo.shouldReleaseCapture = false; + // Aux Release - if ( tracker.nonPrimaryReleaseHandler ) { - propagate = tracker.nonPrimaryReleaseHandler( + if ( tracker.nonPrimaryReleaseHandler && + !eventInfo.preventGesture && + !eventInfo.defaultPrevented ) { + eventInfo.preventDefault = true; + + tracker.nonPrimaryReleaseHandler( { eventSource: tracker, - pointerType: gPoints[ 0 ].type, - position: getPointRelativeToAbsolute(gPoints[0].currentPos, tracker.element), + pointerType: gPoint.type, + position: getPointRelativeToAbsolute(gPoint.currentPos, tracker.element), button: buttonChanged, buttons: pointsList.buttons, - isTouchEvent: gPoints[ 0 ].type === 'touch', - originalEvent: event, + isTouchEvent: gPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, preventDefaultAction: false, userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( event ); - } } - // A primary mouse button may have been released while the non-primary button was down - var otherPointsList = tracker.getActivePointersListByType("mouse"); - // Stop tracking the mouse; see https://github.com/openseadragon/openseadragon/pull/1223 - abortContacts(tracker, event, otherPointsList); // No-op if no active pointer - - return false; + return; } - // OS-specific gestures (e.g. swipe up with four fingers in iPadOS 13) - if (typeof gPoints[ 0 ].currentPos === "undefined") { - abortContacts(tracker, event, pointsList); + updateGPoint = pointsList.getById( gPoint.id ); - return false; + if ( updateGPoint ) { + // Update the pointer, stop tracking it if not still in this element + if ( updateGPoint.captured ) { + //updateGPoint.captured = false; // Handled by updatePointerCaptured() + wasCaptured = true; + } + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = gPoint.currentPos; + updateGPoint.currentTime = gPoint.currentTime; + if ( !updateGPoint.insideElement ) { + stopTrackingPointer( tracker, pointsList, updateGPoint ); + } + + releasePoint = updateGPoint.currentPos; + releaseTime = updateGPoint.currentTime; + } else { + // should never get here...we'll start to track pointer anyway + $.console.warn('updatePointerUp(): pointerup on untracked gPoint'); + gPoint.captured = false; // Handled by updatePointerCaptured() + gPoint.insideElementPressed = false; + gPoint.insideElement = true; + startTrackingPointer( pointsList, gPoint ); + + updateGPoint = gPoint; } - 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 ); + if ( !eventInfo.preventGesture && !eventInfo.defaultPrevented ) { + if ( wasCaptured ) { + // Pointer was activated in our element but could have been removed in any element since events are captured to our element + + eventInfo.shouldReleaseCapture = true; + eventInfo.preventDefault = true; + + pointsList.removeContact(); + //$.console.log('contacts-- ', pointsList.contacts); + + if ( tracker.dragHandler || tracker.dragEndHandler || tracker.pinchHandler ) { + $.MouseTracker.gesturePointVelocityTracker.removePoint( tracker, updateGPoint ); } - releasePoint = updateGPoint.currentPos; - releaseTime = updateGPoint.currentTime; + if ( pointsList.contacts === 0 ) { - if ( wasCaptured ) { - // Pointer was activated in our element but could have been removed in any element since events are captured to our element - - pointsList.removeContact(); - //$.console.log('contacts-- ', 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 / Double-Click - if ( ( tracker.clickHandler || tracker.dblClickHandler ) && updateGPoint.insideElement ) { - quick = releaseTime - updateGPoint.contactTime <= tracker.clickTimeThreshold && - updateGPoint.contactPos.distanceTo( releasePoint ) <= tracker.clickDistThreshold; - - // Click - if ( tracker.clickHandler ) { - 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 ); - } - } - - // Double-Click - if ( tracker.dblClickHandler && quick ) { - pointsList.clicks++; - if ( pointsList.clicks === 1 ) { - delegate.lastClickPos = releasePoint; - /*jshint loopfunc:true*/ - delegate.dblClickTimeOut = setTimeout( function() { - pointsList.clicks = 0; - }, tracker.dblClickTimeThreshold ); - /*jshint loopfunc:false*/ - } else if ( pointsList.clicks === 2 ) { - clearTimeout( delegate.dblClickTimeOut ); - pointsList.clicks = 0; - if ( delegate.lastClickPos.distanceTo( releasePoint ) <= tracker.dblClickDistThreshold ) { - propagate = tracker.dblClickHandler( - { - eventSource: tracker, - pointerType: updateGPoint.type, - position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), - shift: event.shiftKey, - isTouchEvent: updateGPoint.type === 'touch', - originalEvent: event, - preventDefaultAction: false, - userData: tracker.userData - } - ); - if ( propagate === false ) { - $.cancelEvent( event ); - } - } - delegate.lastClickPos = null; - } - } - } - } 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) + // Release (pressed in our element) if ( tracker.releaseHandler ) { - propagate = tracker.releaseHandler( + tracker.releaseHandler( { eventSource: tracker, pointerType: updateGPoint.type, @@ -3121,20 +3424,117 @@ insideElementPressed: updateGPoint.insideElementPressed, insideElementReleased: updateGPoint.insideElement, isTouchEvent: updateGPoint.type === 'touch', - originalEvent: event, + originalEvent: eventInfo.originalEvent, preventDefaultAction: false, userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( event ); + } + + // Drag End + if ( tracker.dragEndHandler ) { + tracker.dragEndHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), + speed: updateGPoint.speed, + direction: updateGPoint.direction, + shift: eventInfo.originalEvent.shiftKey, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, + preventDefaultAction: false, + userData: tracker.userData + } + ); + } + + // Click / Double-Click + if ( ( tracker.clickHandler || tracker.dblClickHandler ) && updateGPoint.insideElement ) { + quick = releaseTime - updateGPoint.contactTime <= tracker.clickTimeThreshold && + updateGPoint.contactPos.distanceTo( releasePoint ) <= tracker.clickDistThreshold; + + // Click + if ( tracker.clickHandler ) { + tracker.clickHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), + quick: quick, + shift: eventInfo.originalEvent.shiftKey, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, + preventDefaultAction: false, + userData: tracker.userData + } + ); + } + + // Double-Click + if ( tracker.dblClickHandler && quick ) { + pointsList.clicks++; + if ( pointsList.clicks === 1 ) { + delegate.lastClickPos = releasePoint; + /*jshint loopfunc:true*/ + delegate.dblClickTimeOut = setTimeout( function() { + pointsList.clicks = 0; + }, tracker.dblClickTimeThreshold ); + /*jshint loopfunc:false*/ + } else if ( pointsList.clicks === 2 ) { + clearTimeout( delegate.dblClickTimeOut ); + pointsList.clicks = 0; + if ( delegate.lastClickPos.distanceTo( releasePoint ) <= tracker.dblClickDistThreshold ) { + tracker.dblClickHandler( + { + eventSource: tracker, + pointerType: updateGPoint.type, + position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), + shift: eventInfo.originalEvent.shiftKey, + isTouchEvent: updateGPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, + preventDefaultAction: false, + userData: tracker.userData + } + ); + } + delegate.lastClickPos = null; + } } } + } 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 + + eventInfo.shouldReleaseCapture = false; + + // Release (pressed in another element) + if ( tracker.releaseHandler ) { + 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: eventInfo.originalEvent, + preventDefaultAction: false, + userData: tracker.userData + } + ); + eventInfo.preventDefault = true; } } } - - return releaseCapture; } @@ -3146,101 +3546,88 @@ * @inner * @param {OpenSeadragon.MouseTracker} tracker * A reference to the MouseTracker instance. - * @param {Object} event - * A reference to the originating DOM event. - * @param {Array.} gPoints + * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * Processing info for originating DOM event. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint * Gesture points associated with the event. */ - function updatePointersMove( tracker, event, gPoints ) { + function updatePointerMove( tracker, eventInfo, gPoint ) { var delegate = THIS[ tracker.hash ], - pointsList = tracker.getActivePointersListByType( gPoints[ 0 ].type ), - i, - gPointCount = gPoints.length, - curGPoint, + pointsList = tracker.getActivePointersListByType( gPoint.type ), updateGPoint, gPointArray, - delta, - propagate; + delta; - if ( typeof event.buttons !== 'undefined' ) { - pointsList.buttons = event.buttons; + if ( typeof eventInfo.originalEvent.buttons !== 'undefined' ) { + pointsList.buttons = eventInfo.originalEvent.buttons; } - for ( i = 0; i < gPointCount; i++ ) { - curGPoint = gPoints[ i ]; - updateGPoint = pointsList.getById( curGPoint.id ); + updateGPoint = pointsList.getById( gPoint.id ); - if ( updateGPoint ) { - // Already tracking the pointer...update it - if ( Object.prototype.hasOwnProperty.call( curGPoint, '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 ); - } + if ( updateGPoint ) { + // Already tracking the pointer...update it + updateGPoint.lastPos = updateGPoint.currentPos; + updateGPoint.lastTime = updateGPoint.currentTime; + updateGPoint.currentPos = gPoint.currentPos; + updateGPoint.currentTime = gPoint.currentTime; + } else { + // Initialize for tracking and add to the tracking list (no pointerover or pointerdown event occurred before this) + gPoint.captured = false; // Handled by updatePointerCaptured() + gPoint.insideElementPressed = false; + gPoint.insideElement = true; + startTrackingPointer( pointsList, gPoint ); } + eventInfo.shouldCapture = false; + eventInfo.shouldReleaseCapture = false; + // Stop (mouse only) - if ( tracker.stopHandler && gPoints[ 0 ].type === 'mouse' ) { + if ( tracker.stopHandler && gPoint.type === 'mouse' ) { clearTimeout( tracker.stopTimeOut ); tracker.stopTimeOut = setTimeout( function() { - handlePointerStop( tracker, event, gPoints[ 0 ].type ); + handlePointerStop( tracker, eventInfo.originalEvent, gPoint.type ); }, tracker.stopDelay ); } if ( pointsList.contacts === 0 ) { // Move (no contacts: hovering mouse or other hover-capable device) if ( tracker.moveHandler ) { - propagate = tracker.moveHandler( + tracker.moveHandler( { eventSource: tracker, - pointerType: gPoints[ 0 ].type, - position: getPointRelativeToAbsolute( gPoints[ 0 ].currentPos, tracker.element ), + pointerType: gPoint.type, + position: getPointRelativeToAbsolute( gPoint.currentPos, tracker.element ), buttons: pointsList.buttons, - isTouchEvent: gPoints[ 0 ].type === 'touch', - originalEvent: event, + isTouchEvent: gPoint.type === 'touch', + originalEvent: eventInfo.originalEvent, 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( + tracker.moveHandler( { eventSource: tracker, pointerType: updateGPoint.type, position: getPointRelativeToAbsolute( updateGPoint.currentPos, tracker.element ), buttons: pointsList.buttons, isTouchEvent: updateGPoint.type === 'touch', - originalEvent: event, + originalEvent: eventInfo.originalEvent, preventDefaultAction: false, userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( event ); - } } // Drag - if ( tracker.dragHandler ) { + if ( tracker.dragHandler && !eventInfo.preventGesture && !eventInfo.defaultPrevented ) { updateGPoint = pointsList.asArray()[ 0 ]; delta = updateGPoint.currentPos.minus( updateGPoint.lastPos ); - propagate = tracker.dragHandler( + tracker.dragHandler( { eventSource: tracker, pointerType: updateGPoint.type, @@ -3249,47 +3636,43 @@ delta: delta, speed: updateGPoint.speed, direction: updateGPoint.direction, - shift: event.shiftKey, + shift: eventInfo.originalEvent.shiftKey, isTouchEvent: updateGPoint.type === 'touch', - originalEvent: event, + originalEvent: eventInfo.originalEvent, preventDefaultAction: false, userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( event ); - } + eventInfo.preventDefault = true; } } else if ( pointsList.contacts === 2 ) { // Move (2 contacts, use center) if ( tracker.moveHandler ) { gPointArray = pointsList.asArray(); - propagate = tracker.moveHandler( + 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, + originalEvent: eventInfo.originalEvent, preventDefaultAction: false, userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( event ); - } } // Pinch - if ( tracker.pinchHandler && gPoints[ 0 ].type === 'touch' ) { + if ( tracker.pinchHandler && gPoint.type === 'touch' && + !eventInfo.preventGesture && !eventInfo.defaultPrevented ) { 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( + tracker.pinchHandler( { eventSource: tracker, pointerType: 'touch', @@ -3298,15 +3681,13 @@ center: getPointRelativeToAbsolute( delegate.currentPinchCenter, tracker.element ), lastDistance: delegate.lastPinchDist, distance: delegate.currentPinchDist, - shift: event.shiftKey, - originalEvent: event, + shift: eventInfo.originalEvent.shiftKey, + originalEvent: eventInfo.originalEvent, preventDefaultAction: false, userData: tracker.userData } ); - if ( propagate === false ) { - $.cancelEvent( event ); - } + eventInfo.preventDefault = true; } } } @@ -3319,14 +3700,20 @@ * @inner * @param {OpenSeadragon.MouseTracker} tracker * A reference to the MouseTracker instance. - * @param {Object} event - * A reference to the originating DOM event. - * @param {Array.} gPoints + * @param {OpenSeadragon.MouseTracker.EventProcessInfo} eventInfo + * Processing info for originating DOM event. + * @param {OpenSeadragon.MouseTracker.GesturePoint} gPoint * Gesture points associated with the event. */ - function updatePointersCancel( tracker, event, gPoints ) { - updatePointersUp( tracker, event, gPoints, 0 ); - updatePointersExit( tracker, event, gPoints ); + function updatePointerCancel( tracker, eventInfo, gPoint ) { + var pointsList = tracker.getActivePointersListByType( gPoint.type ), + updateGPoint; + + updateGPoint = pointsList.getById( gPoint.id ); + + if ( updateGPoint ) { + stopTrackingPointer( tracker, pointsList, updateGPoint ); + } } @@ -3349,32 +3736,4 @@ } } - /** - * True if inside an iframe, otherwise false. - * @member {Boolean} isInIframe - * @private - * @inner - */ - var isInIframe = (function() { - try { - return window.self !== window.top; - } catch (e) { - return true; - } - })(); - - /** - * @function - * @private - * @inner - * @returns {Boolean} True if the target has access rights to events, otherwise false. - */ - function canAccessEvents (target) { - try { - return target.addEventListener && target.removeEventListener; - } catch (e) { - return false; - } - } - }(OpenSeadragon)); diff --git a/src/navigator.js b/src/navigator.js index f0bdf452..cbe0780e 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -167,12 +167,16 @@ $.Navigator = function( options ){ style.zIndex = 999999999; style.cursor = 'default'; }( this.displayRegion.style, this.borderWidth )); + $.setElementPointerEventsNone( this.displayRegion ); + $.setElementTouchActionNone( this.displayRegion ); this.displayRegionContainer = $.makeNeutralElement("div"); this.displayRegionContainer.id = this.element.id + '-displayregioncontainer'; this.displayRegionContainer.className = "displayregioncontainer"; this.displayRegionContainer.style.width = "100%"; this.displayRegionContainer.style.height = "100%"; + $.setElementPointerEventsNone( this.displayRegionContainer ); + $.setElementTouchActionNone( this.displayRegionContainer ); viewer.addControl( this.element, @@ -221,12 +225,22 @@ $.Navigator = function( options ){ // Remove the base class' (Viewer's) innerTracker and replace it with our own this.innerTracker.destroy(); this.innerTracker = new $.MouseTracker({ - element: this.element, + userData: 'Navigator.innerTracker', + element: this.element, //this.canvas, dragHandler: $.delegate( this, onCanvasDrag ), clickHandler: $.delegate( this, onCanvasClick ), releaseHandler: $.delegate( this, onCanvasRelease ), scrollHandler: $.delegate( this, onCanvasScroll ) }); + this.outerTracker.userData = 'Navigator.outerTracker'; + + // this.innerTracker is attached to this.element...we need to allow pointer + // events to pass through this Viewer's canvas/container elements so implicit + // pointer capture works on touch devices + //TODO an alternative is to attach the new MouseTracker to this.canvas...not + // sure why it isn't already (see MouseTracker constructor call above) + $.setElementPointerEventsNone( this.canvas ); + $.setElementPointerEventsNone( this.container ); this.addHandler("reset-size", function() { if (_this.viewport) { @@ -526,7 +540,7 @@ function onCanvasDrag( event ) { * @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. - * @property {Boolean} preventDefaultAction - Set to true to prevent default click to zoom behaviour. Default: false. + * @property {Boolean} preventDefaultAction - Set to true to prevent default drag to pan behaviour. Default: false. */ this.viewer.raiseEvent('navigator-drag', canvasDragEventArgs); diff --git a/src/openseadragon.js b/src/openseadragon.js index 1a6eaa7a..9eaf16c6 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -381,7 +381,11 @@ * 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. + * Sets the zoom amount per second when zoomIn/zoomOut buttons are pressed and held. + * The value is a factor of the current zoom, so 1.0 (the default) disables zooming when the zoomIn/zoomOut buttons + * are held. Higher values will increase the rate of zoom when the zoomIn/zoomOut buttons are held. Note that values + * < 1.0 will reverse the operation of the zoomIn/zoomOut buttons (zoomIn button will decrease the zoom, zoomOut will + * increase the zoom). * * @property {Boolean} [showNavigator=false] * Set to true to make the navigator minimap appear. @@ -918,9 +922,62 @@ function OpenSeadragon( options ){ }; /** - * @returns {Number} Return a ratio comparing the device screen's pixel - * density to the canvas's backing store pixel density, clamped to a - * minimum of 1. Defaults to 1 if canvas isn't supported by the browser. + * True if the browser supports the EventTarget.addEventListener() method + * @member {Boolean} supportsAddEventListener + * @memberof OpenSeadragon + */ + $.supportsAddEventListener = (function () { + return !!(document.documentElement.addEventListener && document.addEventListener); + }()); + + /** + * True if the browser supports the EventTarget.removeEventListener() method + * @member {Boolean} supportsRemoveEventListener + * @memberof OpenSeadragon + */ + $.supportsRemoveEventListener = (function () { + return !!(document.documentElement.removeEventListener && document.removeEventListener); + }()); + + /** + * True if the browser supports the newer EventTarget.addEventListener options argument + * @member {Boolean} supportsEventListenerOptions + * @memberof OpenSeadragon + */ + $.supportsEventListenerOptions = (function () { + var supported = 0; + + if ( $.supportsAddEventListener ) { + try { + var options = { + get capture() { + supported++; + return false; + }, + get once() { + supported++; + return false; + }, + get passive() { + supported++; + return false; + } + }; + window.addEventListener("test", null, options); + window.removeEventListener("test", null, options); + } catch ( e ) { + supported = 0; + } + } + + return supported >= 3; + }()); + + /** + * A ratio comparing the device screen's pixel density to the canvas's backing store pixel density, + * clamped to a minimum of 1. Defaults to 1 if canvas isn't supported by the browser. + * @member {Number} pixelDensityRatio + * @memberof OpenSeadragon */ $.getCurrentPixelDensityRatio = function() { if ( $.supportsCanvas ) { @@ -947,7 +1004,7 @@ function OpenSeadragon( options ){ /** * This closure defines all static methods available to the OpenSeadragon - * namespace. Many, if not most, are taked directly from jQuery for use + * namespace. Many, if not most, are taken directly from jQuery for use * to simplify and reduce common programming patterns. More static methods * from jQuery may eventually make their way into this though we are * attempting to avoid an explicit dependency on jQuery only because @@ -1320,6 +1377,8 @@ function OpenSeadragon( options ){ * @property {Number} SAFARI * @property {Number} CHROME * @property {Number} OPERA + * @property {Number} EDGE + * @property {Number} CHROMEEDGE */ BROWSERS: { UNKNOWN: 0, @@ -1327,7 +1386,9 @@ function OpenSeadragon( options ){ FIREFOX: 2, SAFARI: 3, CHROME: 4, - OPERA: 5 + OPERA: 5, + EDGE: 6, + CHROMEEDGE: 7 }, @@ -1522,29 +1583,6 @@ function OpenSeadragon( options ){ }, - /** - * Gets the latest event, really only useful internally since its - * specific to IE behavior. - * @function - * @param {Event} [event] - * @returns {Event} - * @deprecated For internal use only - * @private - */ - getEvent: function( event ) { - if( event ){ - $.getEvent = function( event ) { - return event; - }; - } else { - $.getEvent = function() { - return window.event; - }; - } - return $.getEvent( event ); - }, - - /** * Gets the position of the mouse on the screen for a given event. * @function @@ -1557,7 +1595,6 @@ function OpenSeadragon( options ){ $.getMousePosition = function( event ){ var result = new $.Point(); - event = $.getEvent( event ); result.x = event.pageX; result.y = event.pageY; @@ -1567,7 +1604,6 @@ function OpenSeadragon( options ){ $.getMousePosition = function( event ){ var result = new $.Point(); - event = $.getEvent( event ); result.x = event.clientX + document.body.scrollLeft + @@ -1803,51 +1839,16 @@ function OpenSeadragon( options ){ /** * Ensures an image is loaded correctly to support alpha transparency. - * Generally only IE has issues doing this correctly for formats like - * png. * @function * @param {String} src * @returns {Element} */ makeTransparentImage: function( src ) { + var img = $.makeNeutralElement( "img" ); - $.makeTransparentImage = function( src ){ - var img = $.makeNeutralElement( "img" ); + img.src = src; - img.src = src; - - return img; - }; - - if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 7 ) { - - $.makeTransparentImage = function( src ){ - var img = $.makeNeutralElement( "img" ), - element = null; - - element = $.makeNeutralElement("span"); - element.style.display = "inline-block"; - - img.onload = function() { - element.style.width = element.style.width || img.width + "px"; - element.style.height = element.style.height || img.height + "px"; - - img.onload = null; - img = null; // to prevent memory leaks in IE - }; - - img.src = src; - element.style.filter = - "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + - src + - "', sizingMethod='scale')"; - - return element; - }; - - } - - return $.makeTransparentImage( src ); + return img; }, @@ -1898,6 +1899,19 @@ function OpenSeadragon( options ){ }, + /** + * Sets the specified element's pointer-events style attribute to 'none'. + * @function + * @param {Element|String} element + */ + setElementPointerEventsNone: function( element ) { + element = $.getElement( element ); + if ( typeof element.style.pointerEvents !== 'undefined' ) { + element.style.pointerEvents = 'none'; + } + }, + + /** * Add the specified CSS class to the element if not present. * @function @@ -1983,6 +1997,34 @@ function OpenSeadragon( options ){ element.className = newClasses.join(' '); }, + /** + * Convert passed addEventListener() options to boolean or options object, + * depending on browser support. + * @function + * @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object + * @param {Boolean} [options.capture] + * @param {Boolean} [options.passive] + * @param {Boolean} [options.once] + * @return {String} The protocol (http:, https:, file:, ftp: ...) + */ + normalizeEventListenerOptions: function (options) { + var opts; + if ( typeof options !== 'undefined' ) { + if ( typeof options === 'boolean' ) { + // Legacy Boolean useCapture + opts = $.supportsEventListenerOptions ? { capture: options } : options; + } else { + // Options object + opts = $.supportsEventListenerOptions ? options : + ( ( typeof options.capture !== 'undefined' ) ? options.capture : false ); + } + } else { + // No options specified - Legacy optional useCapture argument + // (for IE, first supported on version 9, so we'll pass a Boolean) + opts = $.supportsEventListenerOptions ? { capture: false } : false; + } + return opts; + }, /** * Adds an event listener for the given element, eventName and handler. @@ -1990,16 +2032,20 @@ function OpenSeadragon( options ){ * @param {Element|String} element * @param {String} eventName * @param {Function} handler - * @param {Boolean} [useCapture] + * @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object + * @param {Boolean} [options.capture] + * @param {Boolean} [options.passive] + * @param {Boolean} [options.once] */ addEvent: (function () { - if ( window.addEventListener ) { - return function ( element, eventName, handler, useCapture ) { + if ( $.supportsAddEventListener ) { + return function ( element, eventName, handler, options ) { + options = $.normalizeEventListenerOptions(options); element = $.getElement( element ); - element.addEventListener( eventName, handler, useCapture ); + element.addEventListener( eventName, handler, options ); }; - } else if ( window.attachEvent ) { - return function ( element, eventName, handler, useCapture ) { + } else if ( document.documentElement.attachEvent && document.attachEvent ) { + return function ( element, eventName, handler ) { element = $.getElement( element ); element.attachEvent( 'on' + eventName, handler ); }; @@ -2016,16 +2062,18 @@ function OpenSeadragon( options ){ * @param {Element|String} element * @param {String} eventName * @param {Function} handler - * @param {Boolean} [useCapture] + * @param {Boolean|Object} [options] Boolean useCapture, or if [supportsEventListenerOptions]{@link OpenSeadragon.supportsEventListenerOptions}, can be an object + * @param {Boolean} [options.capture] */ removeEvent: (function () { - if ( window.removeEventListener ) { - return function ( element, eventName, handler, useCapture ) { + if ( $.supportsRemoveEventListener ) { + return function ( element, eventName, handler, options ) { + options = $.normalizeEventListenerOptions(options); element = $.getElement( element ); - element.removeEventListener( eventName, handler, useCapture ); + element.removeEventListener( eventName, handler, options ); }; - } else if ( window.detachEvent ) { - return function( element, eventName, handler, useCapture ) { + } else if ( document.documentElement.detachEvent && document.detachEvent ) { + return function( element, eventName, handler ) { element = $.getElement( element ); element.detachEvent( 'on' + eventName, handler ); }; @@ -2042,49 +2090,28 @@ function OpenSeadragon( options ){ * @param {Event} [event] */ cancelEvent: function( event ) { - event = $.getEvent( event ); - - if ( event.preventDefault ) { - $.cancelEvent = function( event ){ - // W3C for preventing default - event.preventDefault(); - }; - } else { - $.cancelEvent = function( event ){ - event = $.getEvent( event ); - // legacy for preventing default - event.cancel = true; - // IE for preventing default - event.returnValue = false; - }; - } - $.cancelEvent( event ); + event.preventDefault(); }, /** - * Stops the propagation of the event up the DOM. + * Returns true if {@link OpenSeadragon.cancelEvent|cancelEvent} has been called on + * the event, otherwise returns false. + * @function + * @param {Event} [event] + */ + eventIsCanceled: function( event ) { + return event.defaultPrevented; + }, + + + /** + * Stops the propagation of the event through the DOM in the capturing and bubbling phases. * @function * @param {Event} [event] */ stopEvent: function( event ) { - event = $.getEvent( event ); - - if ( event.stopPropagation ) { - // W3C for stopping propagation - $.stopEvent = function( event ){ - event.stopPropagation(); - }; - } else { - // IE for stopping propagation - $.stopEvent = function( event ){ - event = $.getEvent( event ); - event.cancelBubble = true; - }; - - } - - $.stopEvent( event ); + event.stopPropagation(); }, @@ -2273,62 +2300,12 @@ function OpenSeadragon( options ){ request.send(null); } catch (e) { - var msg = e.message; - - /* - IE < 10 does not support CORS and an XHR request to a different origin will fail as soon - as send() is called. This is particularly easy to miss during development and appear in - production if you use a CDN or domain sharding and the security policy is likely to break - exception handlers since any attempt to access a property of the request object will - raise an access denied TypeError inside the catch block. - - To be friendlier, we'll check for this specific error and add a documentation pointer - to point developers in the right direction. We test the exception number because IE's - error messages are localized. - */ - var oldIE = $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 10; - if ( oldIE && typeof ( e.number ) !== "undefined" && e.number === -2147024891 ) { - msg += "\nSee http://msdn.microsoft.com/en-us/library/ms537505(v=vs.85).aspx#xdomain"; - } - - $.console.log( "%s while making AJAX request: %s", e.name, msg ); + $.console.log( "%s while making AJAX request: %s", e.name, e.message ); request.onreadystatechange = function(){}; - if (window.XDomainRequest) { // IE9 or IE8 might as well try to use XDomainRequest - var xdr = new window.XDomainRequest(); - if (xdr) { - xdr.onload = function (e) { - if ( $.isFunction( onSuccess ) ) { - onSuccess({ // Faking an xhr object - responseText: xdr.responseText, - status: 200, // XDomainRequest doesn't support status codes, so we just fake one! :/ - statusText: 'OK' - }); - } - }; - xdr.onerror = function (e) { - if ($.isFunction(onError)) { - onError({ // Faking an xhr object - responseText: xdr.responseText, - status: 444, // 444 No Response - statusText: 'An error happened. Due to an XDomainRequest deficiency we can not extract any information about this error. Upgrade your browser.' - }); - } - }; - try { - xdr.open('GET', url); - xdr.send(); - } catch (e2) { - if ( $.isFunction( onError ) ) { - onError( request, e ); - } - } - } - } else { - if ( $.isFunction( onError ) ) { - onError( request, e ); - } + if ( $.isFunction( onError ) ) { + onError( request, e ); } } @@ -2467,16 +2444,7 @@ function OpenSeadragon( options ){ * @returns {Object} */ parseJSON: function(string) { - if (window.JSON && window.JSON.parse) { - $.parseJSON = window.JSON.parse; - } else { - // Should only be used by IE8 in non standards mode - $.parseJSON = function(string) { - /*jshint evil:true*/ - //eslint-disable-next-line no-eval - return eval('(' + string + ')'); - }; - } + $.parseJSON = window.JSON.parse; return $.parseJSON(string); }, @@ -2574,7 +2542,17 @@ function OpenSeadragon( options ){ break; case "Netscape": if (window.addEventListener) { - if ( ua.indexOf( "Firefox" ) >= 0 ) { + if ( ua.indexOf( "Edge" ) >= 0 ) { + $.Browser.vendor = $.BROWSERS.EDGE; + $.Browser.version = parseFloat( + ua.substring( ua.indexOf( "Edge" ) + 5 ) + ); + } else if ( ua.indexOf( "Edg" ) >= 0 ) { + $.Browser.vendor = $.BROWSERS.CHROMEEDGE; + $.Browser.version = parseFloat( + ua.substring( ua.indexOf( "Edg" ) + 4 ) + ); + } else if ( ua.indexOf( "Firefox" ) >= 0 ) { $.Browser.vendor = $.BROWSERS.FIREFOX; $.Browser.version = parseFloat( ua.substring( ua.indexOf( "Firefox" ) + 8 ) @@ -2628,21 +2606,15 @@ function OpenSeadragon( options ){ //determine if this browser supports image alpha transparency $.Browser.alpha = !( - ( - $.Browser.vendor === $.BROWSERS.IE && - $.Browser.version < 9 - ) || ( - $.Browser.vendor === $.BROWSERS.CHROME && - $.Browser.version < 2 - ) + $.Browser.vendor === $.BROWSERS.CHROME && $.Browser.version < 2 ); //determine if this browser supports element.style.opacity - $.Browser.opacity = !( - $.Browser.vendor === $.BROWSERS.IE && - $.Browser.version < 9 - ); + $.Browser.opacity = true; + if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 11 ) { + $.console.error('Internet Explorer versions < 11 are not supported by OpenSeadragon'); + } })(); diff --git a/src/referencestrip.js b/src/referencestrip.js index 31340e43..ee609057 100644 --- a/src/referencestrip.js +++ b/src/referencestrip.js @@ -85,14 +85,7 @@ $.ReferenceStrip = function ( options ) { scroll: $.DEFAULT_SETTINGS.referenceStripScroll, clickTimeThreshold: $.DEFAULT_SETTINGS.clickTimeThreshold }, options, { - //required overrides - element: this.element, - //These need to be overridden to prevent recursion since - //the navigator is a viewer and a viewer has a navigator - showNavigator: false, - mouseNavEnabled: false, - showNavigationControl: false, - showSequenceControl: false + element: this.element } ); $.extend( this, options ); @@ -119,12 +112,14 @@ $.ReferenceStrip = function ( options ) { $.setElementOpacity( this.element, 0.8 ); this.viewer = viewer; - this.innerTracker = new $.MouseTracker( { + this.tracker = new $.MouseTracker( { + userData: 'ReferenceStrip.tracker', element: this.element, + clickHandler: $.delegate( this, onStripClick ), dragHandler: $.delegate( this, onStripDrag ), scrollHandler: $.delegate( this, onStripScroll ), enterHandler: $.delegate( this, onStripEnter ), - exitHandler: $.delegate( this, onStripExit ), + leaveHandler: $.delegate( this, onStripLeave ), keyDownHandler: $.delegate( this, onKeyDown ), keyHandler: $.delegate( this, onKeyPress ) } ); @@ -189,34 +184,12 @@ $.ReferenceStrip = function ( options ) { element.style.width = _this.panelWidth + 'px'; element.style.height = _this.panelHeight + 'px'; element.style.display = 'inline'; - element.style.float = 'left'; //Webkit + element.style['float'] = 'left'; //Webkit element.style.cssFloat = 'left'; //Firefox element.style.styleFloat = 'left'; //IE element.style.padding = '2px'; $.setElementTouchActionNone( element ); - - element.innerTracker = new $.MouseTracker( { - element: element, - clickTimeThreshold: this.clickTimeThreshold, - clickDistThreshold: this.clickDistThreshold, - pressHandler: function ( event ) { - event.eventSource.dragging = $.now(); - }, - releaseHandler: function ( event ) { - var tracker = event.eventSource, - id = tracker.element.id, - page = Number( id.split( '-' )[2] ), - now = $.now(); - - if ( event.insideElementPressed && - event.insideElementReleased && - tracker.dragging && - ( now - tracker.dragging ) < tracker.clickTimeThreshold ) { - tracker.dragging = null; - viewer.goToPage( page ); - } - } - } ); + $.setElementPointerEventsNone( element ); this.element.appendChild( element ); @@ -230,7 +203,8 @@ $.ReferenceStrip = function ( options ) { }; -$.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototype, /** @lends OpenSeadragon.ReferenceStrip.prototype */{ +/** @lends OpenSeadragon.ReferenceStrip.prototype */ +$.ReferenceStrip.prototype = { /** * @function @@ -277,7 +251,7 @@ $.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototyp } this.currentPage = page; - onStripEnter.call( this, { eventSource: this.innerTracker } ); + onStripEnter.call( this, { eventSource: this.tracker } ); } }, @@ -292,7 +266,6 @@ $.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototyp return false; }, - // Overrides Viewer.destroy destroy: function() { if (this.miniViewers) { for (var key in this.miniViewers) { @@ -300,14 +273,34 @@ $.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototyp } } + this.tracker.destroy(); + if (this.element) { this.element.parentNode.removeChild(this.element); } } -} ); +}; +/** + * @private + * @inner + * @function + */ +function onStripClick( event ) { + if ( event.quick ) { + var page; + + if ( 'horizontal' === this.scroll ) { + page = Math.floor(event.position.x / this.panelWidth); + } else { + page = Math.floor(event.position.y / this.panelHeight); + } + + this.viewer.goToPage( page ); + } +} /** @@ -317,13 +310,14 @@ $.extend( $.ReferenceStrip.prototype, $.EventSource.prototype, $.Viewer.prototyp */ function onStripDrag( event ) { - var offsetLeft = Number( this.element.style.marginLeft.replace( 'px', '' ) ), + this.dragging = true; + if ( this.element ) { + var offsetLeft = Number( this.element.style.marginLeft.replace( 'px', '' ) ), offsetTop = Number( this.element.style.marginTop.replace( 'px', '' ) ), scrollWidth = Number( this.element.style.width.replace( 'px', '' ) ), scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ), viewerSize = $.getElementSize( this.viewer.canvas ); - this.dragging = true; - if ( this.element ) { + if ( 'horizontal' === this.scroll ) { if ( -event.delta.x > 0 ) { //forward @@ -366,12 +360,13 @@ function onStripDrag( event ) { * @function */ function onStripScroll( event ) { - var offsetLeft = Number( this.element.style.marginLeft.replace( 'px', '' ) ), + if ( this.element ) { + var offsetLeft = Number( this.element.style.marginLeft.replace( 'px', '' ) ), offsetTop = Number( this.element.style.marginTop.replace( 'px', '' ) ), scrollWidth = Number( this.element.style.width.replace( 'px', '' ) ), scrollHeight = Number( this.element.style.height.replace( 'px', '' ) ), viewerSize = $.getElementSize( this.viewer.canvas ); - if ( this.element ) { + if ( 'horizontal' === this.scroll ) { if ( event.scroll > 0 ) { //forward @@ -412,7 +407,6 @@ function loadPanels( strip, viewerSize, scroll ) { activePanelsStart, activePanelsEnd, miniViewer, - style, i, element; if ( 'horizontal' === strip.scroll ) { @@ -454,34 +448,14 @@ function loadPanels( strip, viewerSize, scroll ) { ajaxHeaders: strip.viewer.ajaxHeaders, useCanvas: strip.useCanvas } ); - - miniViewer.displayRegion = $.makeNeutralElement( "div" ); - miniViewer.displayRegion.id = element.id + '-displayregion'; - miniViewer.displayRegion.className = 'displayregion'; - - style = miniViewer.displayRegion.style; - style.position = 'relative'; - style.top = '0px'; - style.left = '0px'; - style.fontSize = '0px'; - style.overflow = 'hidden'; - style.float = 'left'; //Webkit - style.cssFloat = 'left'; //Firefox - style.styleFloat = 'left'; //IE - style.zIndex = 999999999; - style.cursor = 'default'; - style.width = ( strip.panelWidth - 4 ) + 'px'; - style.height = ( strip.panelHeight - 4 ) + 'px'; - - // TODO: What is this for? Future keyboard navigation support? - miniViewer.displayRegion.innerTracker = new $.MouseTracker( { - element: miniViewer.displayRegion, - startDisabled: true - } ); - - element.getElementsByTagName( 'div' )[0].appendChild( - miniViewer.displayRegion - ); + // Allow pointer events to pass through miniViewer's canvas/container + // elements so implicit pointer capture works on touch devices + $.setElementPointerEventsNone( miniViewer.canvas ); + $.setElementPointerEventsNone( miniViewer.container ); + // We'll use event delegation from the reference strip element instead of + // handling events on every miniViewer + miniViewer.innerTracker.setTracking( false ); + miniViewer.outerTracker.setTracking( false ); strip.miniViewers[element.id] = miniViewer; @@ -524,7 +498,7 @@ function onStripEnter( event ) { * @inner * @function */ -function onStripExit( event ) { +function onStripLeave( event ) { var element = event.eventSource.element; if ( 'horizontal' === this.scroll ) { diff --git a/src/viewer.js b/src/viewer.js index 397b4257..d6bf83f3 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -267,6 +267,7 @@ $.Viewer = function( options ) { style.top = "0px"; style.textAlign = "left"; // needed to protect against }( this.container.style )); + $.setElementTouchActionNone( this.container ); this.container.insertBefore( this.canvas, this.container.firstChild ); this.element.appendChild( this.container ); @@ -280,12 +281,14 @@ $.Viewer = function( options ) { this.docOverflow = document.documentElement.style.overflow; this.innerTracker = new $.MouseTracker({ + userData: 'Viewer.innerTracker', element: this.canvas, startDisabled: !this.mouseNavEnabled, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, dblClickTimeThreshold: this.dblClickTimeThreshold, dblClickDistThreshold: this.dblClickDistThreshold, + contextMenuHandler: $.delegate( this, onCanvasContextMenu ), keyDownHandler: $.delegate( this, onCanvasKeyDown ), keyHandler: $.delegate( this, onCanvasKeyPress ), clickHandler: $.delegate( this, onCanvasClick ), @@ -293,7 +296,7 @@ $.Viewer = function( options ) { dragHandler: $.delegate( this, onCanvasDrag ), dragEndHandler: $.delegate( this, onCanvasDragEnd ), enterHandler: $.delegate( this, onCanvasEnter ), - exitHandler: $.delegate( this, onCanvasExit ), + leaveHandler: $.delegate( this, onCanvasLeave ), pressHandler: $.delegate( this, onCanvasPress ), releaseHandler: $.delegate( this, onCanvasRelease ), nonPrimaryPressHandler: $.delegate( this, onCanvasNonPrimaryPress ), @@ -303,6 +306,7 @@ $.Viewer = function( options ) { }); this.outerTracker = new $.MouseTracker({ + userData: 'Viewer.outerTracker', element: this.container, startDisabled: !this.mouseNavEnabled, clickTimeThreshold: this.clickTimeThreshold, @@ -310,7 +314,7 @@ $.Viewer = function( options ) { dblClickTimeThreshold: this.dblClickTimeThreshold, dblClickDistThreshold: this.dblClickDistThreshold, enterHandler: $.delegate( this, onContainerEnter ), - exitHandler: $.delegate( this, onContainerExit ) + leaveHandler: $.delegate( this, onContainerLeave ) }); if( this.toolbar ){ @@ -403,20 +407,22 @@ $.Viewer = function( options ) { // Overlay container this.overlaysContainer = $.makeNeutralElement( "div" ); + $.setElementPointerEventsNone( this.overlaysContainer ); + $.setElementTouchActionNone( this.overlaysContainer ); this.canvas.appendChild( this.overlaysContainer ); // Now that we have a drawer, see if it supports rotate. If not we need to remove the rotate buttons if (!this.drawer.canRotate()) { // Disable/remove the rotate left/right buttons since they aren't supported if (this.rotateLeft) { - i = this.buttons.buttons.indexOf(this.rotateLeft); - this.buttons.buttons.splice(i, 1); - this.buttons.element.removeChild(this.rotateLeft.element); + i = this.buttonGroup.buttons.indexOf(this.rotateLeft); + this.buttonGroup.buttons.splice(i, 1); + this.buttonGroup.element.removeChild(this.rotateLeft.element); } if (this.rotateRight) { - i = this.buttons.buttons.indexOf(this.rotateRight); - this.buttons.buttons.splice(i, 1); - this.buttons.element.removeChild(this.rotateRight.element); + i = this.buttonGroup.buttons.indexOf(this.rotateRight); + this.buttonGroup.buttons.splice(i, 1); + this.buttonGroup.element.removeChild(this.rotateRight.element); } } @@ -777,8 +783,12 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, this.removeAllHandlers(); - if (this.buttons) { - this.buttons.destroy(); + if (this.buttonGroup) { + this.buttonGroup.destroy(); + } else if (this.customButtons) { + while (this.customButtons.length) { + this.customButtons.pop().destroy(); + } } if (this.paging) { @@ -1102,7 +1112,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, THIS[ this.hash ].fullPage = false; // mouse will likely be outside now - $.delegate( this, onContainerExit )( { } ); + $.delegate( this, onContainerLeave )( { } ); } @@ -1866,13 +1876,13 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } if ( useGroup ) { - this.buttons = new $.ButtonGroup({ + this.buttonGroup = new $.ButtonGroup({ buttons: buttons, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold }); - this.navControl = this.buttons.element; + this.navControl = this.buttonGroup.element; this.addHandler( 'open', $.delegate( this, lightUp ) ); if( this.toolbar ){ @@ -1886,6 +1896,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, {anchor: this.navigationControlAnchor || $.ControlAnchor.TOP_LEFT} ); } + } else { + this.customButtons = buttons; } } @@ -2550,6 +2562,26 @@ function onBlur(){ } +function onCanvasContextMenu( event ) { + /** + * Raised when a contextmenu event occurs in the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-contextmenu + * @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 {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-contextmenu', { + tracker: event.eventSource, + position: event.position, + originalEvent: event.originalEvent + }); +} + function onCanvasKeyDown( event ) { var canvasKeyDownEventArgs = { originalEvent: event.originalEvent, @@ -2799,6 +2831,7 @@ function onCanvasDrag( event ) { var canvasDragEventArgs = { tracker: event.eventSource, + pointerType: event.pointerType, position: event.position, delta: event.delta, speed: event.speed, @@ -2816,13 +2849,14 @@ function onCanvasDrag( event ) { * @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 {String} pointerType - "mouse", "touch", "pen", etc. * @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 {Boolean} preventDefaultAction - Set to true to prevent default drag behaviour. Default: false. + * @property {Boolean} preventDefaultAction - Set to true to prevent default drag to pan behaviour. Default: false. * @property {?Object} userData - Arbitrary subscriber-defined object. */ this.raiseEvent( 'canvas-drag', canvasDragEventArgs); @@ -2895,6 +2929,7 @@ function onCanvasDragEnd( event ) { * @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 {String} pointerType - "mouse", "touch", "pen", etc. * @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. @@ -2904,6 +2939,7 @@ function onCanvasDragEnd( event ) { */ this.raiseEvent('canvas-drag-end', { tracker: event.eventSource, + pointerType: event.pointerType, position: event.position, speed: event.speed, direction: event.direction, @@ -2942,12 +2978,7 @@ function onCanvasEnter( event ) { }); } -function onCanvasExit( event ) { - - if (window.location !== window.parent.location){ - $.MouseTracker.resetAllMouseTrackers(); - } - +function onCanvasLeave( event ) { /** * Raised when a pointer leaves the {@link OpenSeadragon.Viewer#canvas} element. * @@ -3126,6 +3157,7 @@ function onCanvasPinch( event ) { * @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 {String} pointerType - "mouse", "touch", "pen", etc. * @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. @@ -3137,6 +3169,7 @@ function onCanvasPinch( event ) { */ this.raiseEvent('canvas-pinch', { tracker: event.eventSource, + pointerType: event.pointerType, gesturePoints: event.gesturePoints, lastCenter: event.lastCenter, center: event.center, @@ -3225,6 +3258,7 @@ function onContainerEnter( event ) { * @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 {String} pointerType - "mouse", "touch", "pen", etc. * @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 {Number} pointers - Number of pointers (all types) active in the tracked element. @@ -3235,6 +3269,7 @@ function onContainerEnter( event ) { */ this.raiseEvent( 'container-enter', { tracker: event.eventSource, + pointerType: event.pointerType, position: event.position, buttons: event.buttons, pointers: event.pointers, @@ -3244,7 +3279,7 @@ function onContainerEnter( event ) { }); } -function onContainerExit( event ) { +function onContainerLeave( event ) { if ( event.pointers < 1 ) { THIS[ this.hash ].mouseInside = false; if ( !THIS[ this.hash ].animating ) { @@ -3259,6 +3294,7 @@ function onContainerExit( event ) { * @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 {String} pointerType - "mouse", "touch", "pen", etc. * @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 {Number} pointers - Number of pointers (all types) active in the tracked element. @@ -3269,6 +3305,7 @@ function onContainerExit( event ) { */ this.raiseEvent( 'container-exit', { tracker: event.eventSource, + pointerType: event.pointerType, position: event.position, buttons: event.buttons, pointers: event.pointers, @@ -3499,8 +3536,10 @@ function doSingleZoomOut() { function lightUp() { - this.buttons.emulateEnter(); - this.buttons.emulateExit(); + if (this.buttonGroup) { + this.buttonGroup.emulateEnter(); + this.buttonGroup.emulateLeave(); + } } @@ -3519,8 +3558,8 @@ function onFullScreen() { this.setFullScreen( !this.isFullPage() ); } // correct for no mouseout event on change - if ( this.buttons ) { - this.buttons.emulateExit(); + if ( this.buttonGroup ) { + this.buttonGroup.emulateLeave(); } this.fullPageButton.element.focus(); if ( this.viewport ) { diff --git a/test/helpers/legacy.mouse.shim.js b/test/helpers/legacy.mouse.shim.js index 3609ed85..19374147 100644 --- a/test/helpers/legacy.mouse.shim.js +++ b/test/helpers/legacy.mouse.shim.js @@ -6,20 +6,22 @@ $.MouseTracker.subscribeEvents = [ "click", "dblclick", "keypress", "focus", "blur", $.MouseTracker.wheelEventName ]; - if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) { + if( $.MouseTracker.wheelEventName === "DOMMouseScroll" ) { // Older Firefox $.MouseTracker.subscribeEvents.push( "MozMousePixelScroll" ); } $.MouseTracker.havePointerEvents = false; - if ( $.Browser.vendor === $.BROWSERS.IE && $.Browser.version < 9 ) { - $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave" ); - $.MouseTracker.haveMouseEnter = true; - } else { - $.MouseTracker.subscribeEvents.push( "mouseover", "mouseout" ); - $.MouseTracker.haveMouseEnter = false; + $.MouseTracker.subscribeEvents.push( "mouseenter", "mouseleave", "mouseover", "mouseout", "mousedown", "mouseup", "mousemove" ); + $.MouseTracker.mousePointerId = "legacy-mouse"; + // Legacy mouse events capture support (IE/Firefox only?) + $.MouseTracker.havePointerCapture = (function () { + var divElement = document.createElement( 'div' ); + return $.isFunction( divElement.setCapture ) && $.isFunction( divElement.releaseCapture ); + }()); + if ( $.MouseTracker.havePointerCapture ) { + $.MouseTracker.subscribeEvents.push( "losecapture" ); } - $.MouseTracker.subscribeEvents.push( "mousedown", "mouseup", "mousemove" ); if ( 'ontouchstart' in window ) { // iOS, Android, and other W3c Touch Event implementations // (see http://www.w3.org/TR/touch-events/) @@ -32,8 +34,5 @@ // 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/helpers/test.js b/test/helpers/test.js index 8e7685d8..60931bba 100644 --- a/test/helpers/test.js +++ b/test/helpers/test.js @@ -37,7 +37,7 @@ }; $canvas - .simulate( OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseenter' : 'mouseover', event ) + .simulate( 'mouseenter', event ) .simulate( 'mousedown', event ); for ( var i = 0; i < args.dragCount; i++ ) { event.clientX += args.dragDx; @@ -47,7 +47,7 @@ } $canvas .simulate( 'mouseup', event ) - .simulate( OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseleave' : 'mouseout', event ); + .simulate( 'mouseleave', event ); }, // ---------- diff --git a/test/modules/controls.js b/test/modules/controls.js index 57d827c9..0e6bdf7c 100644 --- a/test/modules/controls.js +++ b/test/modules/controls.js @@ -53,9 +53,9 @@ assert.ok(viewer.showZoomControl, 'showZoomControl should be on'); assert.ok(!!viewer.zoomInButton, "zoomIn button should not be null"); assert.ok(!!viewer.zoomOutButton, "zoomOut button should not be null"); - assert.notEqual(viewer.buttons.buttons.indexOf(viewer.zoomInButton), -1, + assert.notEqual(viewer.buttonGroup.buttons.indexOf(viewer.zoomInButton), -1, "The zoomIn button should be present"); - assert.notEqual(viewer.buttons.buttons.indexOf(viewer.zoomOutButton), -1, + assert.notEqual(viewer.buttonGroup.buttons.indexOf(viewer.zoomOutButton), -1, "The zoomOut button should be present"); var oldZoom = viewer.viewport.getZoom(); @@ -108,7 +108,7 @@ viewer.removeHandler('open', openHandler); assert.ok(viewer.showHomeControl, 'showHomeControl should be on'); assert.ok(!!viewer.homeButton, "Home button should not be null"); - assert.notEqual(viewer.buttons.buttons.indexOf(viewer.homeButton), -1, + assert.notEqual(viewer.buttonGroup.buttons.indexOf(viewer.homeButton), -1, "The home button should be present"); viewer.viewport.zoomBy(1.1); @@ -167,7 +167,7 @@ viewer.removeHandler('open', openHandler); assert.ok(viewer.showHomeControl, 'showFullPageControl should be on'); assert.ok(!!viewer.fullPageButton, "FullPage button should not be null"); - assert.notEqual(viewer.buttons.buttons.indexOf(viewer.fullPageButton), -1, + assert.notEqual(viewer.buttonGroup.buttons.indexOf(viewer.fullPageButton), -1, "The full page button should be present"); assert.ok(!viewer.isFullPage(), "OSD should not be in full page."); @@ -223,9 +223,9 @@ assert.ok(viewer.drawer, 'Drawer exists'); assert.ok(viewer.drawer.canRotate(), 'drawer.canRotate needs to be true'); assert.ok(viewer.showRotationControl, 'showRotationControl should be true'); - assert.notEqual(viewer.buttons.buttons.indexOf(viewer.rotateLeftButton), -1, + assert.notEqual(viewer.buttonGroup.buttons.indexOf(viewer.rotateLeftButton), -1, "rotateLeft should be found"); - assert.notEqual(viewer.buttons.buttons.indexOf(viewer.rotateRightButton), -1, + assert.notEqual(viewer.buttonGroup.buttons.indexOf(viewer.rotateRightButton), -1, "rotateRight should be found"); // Now simulate the left/right button clicks. diff --git a/test/modules/events.js b/test/modules/events.js index 849e86f9..d07a3eea 100644 --- a/test/modules/events.js +++ b/test/modules/events.js @@ -32,7 +32,7 @@ offset = $canvas.offset(), tracker = viewer.innerTracker, origEnterHandler, - origExitHandler, + origLeaveHandler, origPressHandler, origReleaseHandler, origNonPrimaryPressHandler, @@ -43,7 +43,7 @@ origDragHandler, origDragEndHandler, enterCount, - exitCount, + leaveCount, pressCount, releaseCount, rightPressCount, @@ -71,11 +71,11 @@ return true; } }; - origExitHandler = tracker.exitHandler; - tracker.exitHandler = function ( event ) { - exitCount++; - if (origExitHandler) { - return origExitHandler( event ); + origLeaveHandler = tracker.leaveHandler; + tracker.leaveHandler = function ( event ) { + leaveCount++; + if (origLeaveHandler) { + return origLeaveHandler( event ); } else { return true; } @@ -182,7 +182,7 @@ var unhookViewerHandlers = function () { tracker.enterHandler = origEnterHandler; - tracker.exitHandler = origExitHandler; + tracker.leaveHandler = origLeaveHandler; tracker.pressHandler = origPressHandler; tracker.releaseHandler = origReleaseHandler; tracker.moveHandler = origMoveHandler; @@ -195,21 +195,21 @@ var simulateEnter = function (x, y) { simEvent.clientX = offset.left + x; simEvent.clientY = offset.top + y; - $canvas.simulate( OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseenter' : 'mouseover', simEvent ); + $canvas.simulate( 'mouseenter', simEvent ); }; var simulateLeave = function (x, y) { simEvent.clientX = offset.left + x; simEvent.clientY = offset.top + y; simEvent.relatedTarget = document.body; - $canvas.simulate( OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseleave' : 'mouseout', simEvent ); + $canvas.simulate( 'mouseleave', simEvent ); }; //var simulateLeaveFrame = function (x, y) { // simEvent.clientX = offset.left + x; // simEvent.clientY = offset.top + y; // simEvent.relatedTarget = document.getElementsByTagName("html")[0]; - // $canvas.simulate( OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseleave' : 'mouseout', simEvent ); + // $canvas.simulate( 'mouseleave', simEvent ); //}; var simulateDown = function (x, y) { @@ -256,7 +256,7 @@ clientY: offset.top }; enterCount = 0; - exitCount = 0; + leaveCount = 0; pressCount = 0; releaseCount = 0; rightPressCount = 0; @@ -280,8 +280,8 @@ if ('enterCount' in expected) { assert.equal( enterCount, expected.enterCount, expected.description + 'enterHandler event count matches expected (' + expected.enterCount + ')' ); } - if ('exitCount' in expected) { - assert.equal( exitCount, expected.exitCount, expected.description + 'exitHandler event count matches expected (' + expected.exitCount + ')' ); + if ('leaveCount' in expected) { + assert.equal( leaveCount, expected.leaveCount, expected.description + 'leaveHandler event count matches expected (' + expected.leaveCount + ')' ); } if ('pressCount' in expected) { assert.equal( pressCount, expected.pressCount, expected.description + 'pressHandler event count matches expected (' + expected.pressCount + ')' ); @@ -355,7 +355,7 @@ assessGestureExpectations({ description: 'enter-move-release (release in tracked element, press in unknown element): ', enterCount: 1, - exitCount: 0, + leaveCount: 0, pressCount: 0, releaseCount: 1, rightPressCount: 0, @@ -375,16 +375,16 @@ }); simulateLeave(-1, -1); // flush tracked pointer - // enter-move-exit (fly-over) + // enter-move-leave (fly-over) resetForAssessment(); simulateEnter(0, 0); simulateMove(1, 1, 10); simulateMove(-1, -1, 10); simulateLeave(-1, -1); assessGestureExpectations({ - description: 'enter-move-exit (fly-over): ', + description: 'enter-move-leave (fly-over): ', enterCount: 1, - exitCount: 1, + leaveCount: 1, pressCount: 0, releaseCount: 0, rightPressCount: 0, @@ -403,15 +403,15 @@ //quickClick: false }); - // move-exit (fly-over, no enter event) + // move-leave (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): ', + description: 'move-leave (fly-over, no enter event): ', enterCount: 0, - exitCount: 1, + leaveCount: 1, pressCount: 0, releaseCount: 0, rightPressCount: 0, @@ -430,7 +430,7 @@ //quickClick: false }); - // enter-press-release-press-release-exit (primary/left double click) + // enter-press-release-press-release-leave (primary/left double click) resetForAssessment(); simulateEnter(0, 0); simulateDown(0, 0); @@ -439,9 +439,9 @@ simulateUp(0, 0); simulateLeave(-1, -1); assessGestureExpectations({ - description: 'enter-press-release-press-release-exit (primary/left double click): ', + description: 'enter-press-release-press-release-leave (primary/left double click): ', enterCount: 1, - exitCount: 1, + leaveCount: 1, pressCount: 2, releaseCount: 2, rightPressCount: 0, @@ -452,7 +452,7 @@ clickCount: 2, dblClickCount: 1, dragCount: 0, - dragEndCount: 0, + dragEndCount: 2, // v2.5.0+ drag-end event now fired even if pointer didn't move (#1459) insideElementPressed: true, insideElementReleased: true, contacts: 0, @@ -460,16 +460,16 @@ //quickClick: true }); - // enter-press-release-exit (primary/left click) + // enter-press-release-leave (primary/left click) resetForAssessment(); simulateEnter(0, 0); simulateDown(0, 0); simulateUp(0, 0); simulateLeave(-1, -1); assessGestureExpectations({ - description: 'enter-press-release-exit (primary/left click): ', + description: 'enter-press-release-leave (primary/left click): ', enterCount: 1, - exitCount: 1, + leaveCount: 1, pressCount: 1, releaseCount: 1, rightPressCount: 0, @@ -480,7 +480,7 @@ clickCount: 1, dblClickCount: 0, dragCount: 0, - dragEndCount: 0, + dragEndCount: 1, // v2.5.0+ drag-end event now fired even if pointer didn't move (#1459) insideElementPressed: true, insideElementReleased: true, contacts: 0, @@ -488,16 +488,16 @@ quickClick: true }); - // enter-nonprimarypress-nonprimaryrelease-exit (secondary/right click) + // enter-nonprimarypress-nonprimaryrelease-leave (secondary/right click) resetForAssessment(); simulateEnter(0, 0); simulateNonPrimaryDown(0, 0, 2); simulateNonPrimaryUp(0, 0, 2); simulateLeave(-1, -1); assessGestureExpectations({ - description: 'enter-nonprimarypress-nonprimaryrelease-exit (secondary/right click): ', + description: 'enter-nonprimarypress-nonprimaryrelease-leave (secondary/right click): ', enterCount: 1, - exitCount: 1, + leaveCount: 1, pressCount: 0, releaseCount: 0, rightPressCount: 1, @@ -516,16 +516,16 @@ //quickClick: true }); - // enter-nonprimarypress-nonprimaryrelease-exit (aux/middle click) + // enter-nonprimarypress-nonprimaryrelease-leave (aux/middle click) resetForAssessment(); simulateEnter(0, 0); simulateNonPrimaryDown(0, 0, 1); simulateNonPrimaryUp(0, 0, 1); simulateLeave(-1, -1); assessGestureExpectations({ - description: 'enter-nonprimarypress-nonprimaryrelease-exit (aux/middle click): ', + description: 'enter-nonprimarypress-nonprimaryrelease-leave (aux/middle click): ', enterCount: 1, - exitCount: 1, + leaveCount: 1, pressCount: 0, releaseCount: 0, rightPressCount: 0, @@ -544,7 +544,7 @@ //quickClick: true }); - // enter-nonprimarypress-move-nonprimaryrelease-move-exit (secondary/right button drag, release in tracked element) + // enter-nonprimarypress-move-nonprimaryrelease-move-leave (secondary/right button drag, release in tracked element) resetForAssessment(); simulateEnter(0, 0); simulateNonPrimaryDown(0, 0, 2); @@ -553,9 +553,9 @@ simulateMove(-1, -1, 100); simulateLeave(-1, -1); assessGestureExpectations({ - description: 'enter-nonprimarypress-move-nonprimaryrelease-move-exit (secondary/right button drag, release in tracked element): ', + description: 'enter-nonprimarypress-move-nonprimaryrelease-move-leave (secondary/right button drag, release in tracked element): ', enterCount: 1, - exitCount: 1, + leaveCount: 1, pressCount: 0, releaseCount: 0, rightPressCount: 1, @@ -574,7 +574,7 @@ //quickClick: false }); - // enter-press-move-release-move-exit (drag, release in tracked element) + // enter-press-move-release-move-leave (drag, release in tracked element) resetForAssessment(); simulateEnter(0, 0); simulateDown(0, 0); @@ -583,9 +583,9 @@ simulateMove(-1, -1, 100); simulateLeave(-1, -1); assessGestureExpectations({ - description: 'enter-press-move-release-move-exit (drag, release in tracked element): ', + description: 'enter-press-move-release-move-leave (drag, release in tracked element): ', enterCount: 1, - exitCount: 1, + leaveCount: 1, pressCount: 1, releaseCount: 1, rightPressCount: 0, @@ -604,7 +604,7 @@ quickClick: false }); - // enter-press-move-exit-move-release (drag, release outside tracked element) + // enter-press-move-leave-move-release (drag, release outside tracked element) resetForAssessment(); simulateEnter(0, 0); simulateDown(0, 0); @@ -614,9 +614,9 @@ simulateMove(-1, -1, 5); simulateUp(-5, -5); assessGestureExpectations({ - description: 'enter-press-move-exit-move-release (drag, release outside tracked element): ', + description: 'enter-press-move-leave-move-release (drag, release outside tracked element): ', enterCount: 1, - exitCount: 1, + leaveCount: 1, pressCount: 1, releaseCount: 1, rightPressCount: 0, @@ -635,7 +635,7 @@ quickClick: false }); - //// enter-press-move-exit-move-release-outside (drag, release outside iframe) + //// enter-press-move-leave-move-release-outside (drag, release outside iframe) //resetForAssessment(); //simulateEnter(0, 0); //simulateDown(0, 0); @@ -644,9 +644,9 @@ //simulateLeaveFrame(-1, -1); //// you don't actually receive the mouseup if you mouseup outside of the document //assessGestureExpectations({ - // description: 'enter-press-move-exit-move-release-outside (drag, release outside iframe): ', - // enterCount: 1, - // exitCount: 1, + // description: 'enter-press-move-leave-move-release-outside (drag, release outside iframe): ', + // enterCount: 1, + // leaveCount: 1, // pressCount: 1, // releaseCount: 1, // rightPressCount: 0, @@ -953,7 +953,7 @@ dragEndHandler: onMouseTrackerDragEnd, releaseHandler: onMouseTrackerRelease, clickHandler: onMouseTrackerClick, - exitHandler: onMouseTrackerExit + leaveHandler: onMouseTrackerLeave } ); var event = { @@ -1050,7 +1050,7 @@ checkOriginalEventReceived( event ); }; - var onMouseTrackerExit = function ( event ) { + var onMouseTrackerLeave = function ( event ) { checkOriginalEventReceived( event ); mouseTracker.destroy(); diff --git a/test/modules/navigator.js b/test/modules/navigator.js index e7f5f327..e55c5400 100644 --- a/test/modules/navigator.js +++ b/test/modules/navigator.js @@ -216,19 +216,34 @@ clientY: offset.top + locationY }; $canvas - .simulate(OpenSeadragon.MouseTracker.haveMouseEnter ? 'mouseenter' : 'mouseover', event) + .simulate('mouseenter', event) .simulate('mousedown', event) .simulate('mouseup', event); }; var simulateNavigatorDrag = function (viewer, distanceX, distanceY) { - var $canvas = $(viewer.element).find('.displayregion'), - event = { - dx: Math.floor(distanceX), - dy: Math.floor(distanceY) - }; - $canvas - .simulate('drag', event); + var $canvas = $(viewer.element).find('.openseadragon-canvas'), + offset = $canvas.offset(), + event = {}; + + event.clientX = offset.left + 1; + event.clientY = offset.top + 1; + $canvas.simulate( 'mouseenter', event ); + + event.button = 0; + $canvas.simulate( 'mousedown', event ); + + event.clientX += distanceX; + event.clientY += distanceY; + $canvas.simulate( 'mousemove', event ); + + event.button = 0; + $canvas.simulate( 'mouseup', event ); + + event.clientX = offset.left - 1; + event.clientY = offset.top - 1; + event.relatedTarget = document.body; + $canvas.simulate( 'mouseleave', event ); }; var dragNavigatorBackToCenter = function () {