diff --git a/changelog.txt b/changelog.txt index d7211b15..42227a91 100644 --- a/changelog.txt +++ b/changelog.txt @@ -47,6 +47,19 @@ OPENSEADRAGON CHANGELOG * Fixed: DZI tilesource was broken on IE8/IE9 (#563) * Exposed secondary pointer button (middle, right, etc.) events from MouseTracker and through viewer (#479) * MouseTracker - Improved IE 8 compatibility (#562) +* MouseTracker - Improved IE 9+ compatibility (#564) +* MouseTracker - Simulated touchenter/touchleave events now bubble to parent element MouseTrackers (#566) +* MouseTracker - Improved multitouch support in enter/exit event handlers (#566) +* MouseTracker - orphaned tracked touch pointers removed (fix for #539) +* MouseTracker - removed touchenter/touchleave event support since the events don't exist on any known platform and have been removed from the W3C specification (#566) +* Removed Viewer onContainerPress/onContainerRelease handlers (and the associated 'container-release' event ) that were never fired due to the canvas (child) element capturing the DOM events (#566) +* Added 'canvas-enter', 'canvas-exit', and 'canvas-press' events to Viewer (#566) +* ButtonGroup - removed obsolete MouseTracker event handlers (#566) +* MouseTracker - added keydown and keyup handlers (#568) +* Modifier keys ignored in keyboard navigation handlers (#503) +* Requesting keyboard focus when viewer is clicked (#537) +* Arrow key navigation fixed across platforms (#565) +* Removed textarea element from viewer DOM. Viewer.canvas now handles keyboard navigation (#569) 1.2.0: diff --git a/src/buttongroup.js b/src/buttongroup.js index 2837805a..7505af0a 100644 --- a/src/buttongroup.js +++ b/src/buttongroup.js @@ -105,22 +105,6 @@ $.ButtonGroup = function( options ) { } } }, - pressHandler: function ( event ) { - if ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) { - var i; - for ( i = 0; i < _this.buttons.length; i++ ) { - _this.buttons[ i ].notifyGroupEnter(); - } - } - }, - releaseHandler: function ( event ) { - var i; - if ( !event.insideElementReleased || ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) ) { - for ( i = 0; i < _this.buttons.length; i++ ) { - _this.buttons[ i ].notifyGroupExit(); - } - } - } }).setTracking( true ); }; diff --git a/src/mousetracker.js b/src/mousetracker.js index 09b3c751..9e8f25ef 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -34,7 +34,10 @@ (function ( $ ) { - // dictionary from hash to private properties + // All MouseTracker instances + var MOUSETRACKERS = []; + + // dictionary from hash to private properties var THIS = {}; @@ -92,6 +95,10 @@ * An optional handler for after a drag gesture. * @param {OpenSeadragon.EventHandler} [options.pinchHandler=null] * An optional handler for the pinch gesture. + * @param {OpenSeadragon.EventHandler} [options.keyDownHandler=null] + * An optional handler for keydown. + * @param {OpenSeadragon.EventHandler} [options.keyUpHandler=null] + * An optional handler for keyup. * @param {OpenSeadragon.EventHandler} [options.keyHandler=null] * An optional handler for keypress. * @param {OpenSeadragon.EventHandler} [options.focusHandler=null] @@ -103,6 +110,8 @@ */ $.MouseTracker = function ( options ) { + MOUSETRACKERS.push( this ); + var args = arguments; if ( !$.isPlainObject( options ) ) { @@ -165,6 +174,8 @@ this.dragEndHandler = options.dragEndHandler || null; this.pinchHandler = options.pinchHandler || null; this.stopHandler = options.stopHandler || null; + this.keyDownHandler = options.keyDownHandler || null; + this.keyUpHandler = options.keyUpHandler || null; this.keyHandler = options.keyHandler || null; this.focusHandler = options.focusHandler || null; this.blurHandler = options.blurHandler || null; @@ -180,6 +191,8 @@ THIS[ this.hash ] = { click: function ( event ) { onClick( _this, event ); }, dblclick: function ( event ) { onDblClick( _this, event ); }, + keydown: function ( event ) { onKeyDown( _this, event ); }, + keyup: function ( event ) { onKeyUp( _this, event ); }, keypress: function ( event ) { onKeyPress( _this, event ); }, focus: function ( event ) { onFocus( _this, event ); }, blur: function ( event ) { onBlur( _this, event ); }, @@ -199,8 +212,6 @@ mousemove: function ( event ) { onMouseMove( _this, event ); }, mousemovecaptured: function ( event ) { onMouseMoveCaptured( _this, event ); }, - touchenter: function ( event ) { onTouchEnter( _this, event ); }, - touchleave: function ( event ) { onTouchLeave( _this, event ); }, touchstart: function ( event ) { onTouchStart( _this, event ); }, touchend: function ( event ) { onTouchEnd( _this, event ); }, touchendcaptured: function ( event ) { onTouchEndCaptured( _this, event ); }, @@ -215,7 +226,6 @@ 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 ); }, @@ -256,9 +266,18 @@ * @function */ destroy: function () { + var i; + stopTracking( this ); this.element = null; + for ( i = 0; i < MOUSETRACKERS.length; i++ ) { + if ( MOUSETRACKERS[ i ] === this ) { + MOUSETRACKERS.splice( i, 1 ); + break; + } + } + THIS[ this.hash ] = null; delete THIS[ this.hash ]; }, @@ -313,6 +332,24 @@ return list; }, + /** + * Returns the total number of pointers currently active on the tracked element. + * @function + * @returns {Number} + */ + getActivePointerCount: function () { + var delegate = THIS[ this.hash ], + i, + len = delegate.activePointersLists.length, + count = 0; + + for ( i = 0; i < len; i++ ) { + count += delegate.activePointersLists[ i ].getLength(); + } + + return count; + }, + /** * Implement or assign implementation to these handlers during or after * calling the constructor. @@ -327,6 +364,8 @@ * @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. @@ -357,6 +396,8 @@ * @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. @@ -710,8 +751,66 @@ * A reference to the tracker instance. * @param {Number} event.keyCode * The key code that was pressed. + * @param {Boolean} event.ctrl + * True if the ctrl key was pressed during this event. * @param {Boolean} event.shift * True if the shift key was pressed during this event. + * @param {Boolean} event.alt + * True if the alt key was pressed during this event. + * @param {Boolean} event.meta + * True if the meta key was pressed during this event. + * @param {Object} event.originalEvent + * The original event object. + * @param {Boolean} event.preventDefaultAction + * Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + keyDownHandler: 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 {Number} event.keyCode + * The key code that was pressed. + * @param {Boolean} event.ctrl + * True if the ctrl key was pressed during this event. + * @param {Boolean} event.shift + * True if the shift key was pressed during this event. + * @param {Boolean} event.alt + * True if the alt key was pressed during this event. + * @param {Boolean} event.meta + * True if the meta key was pressed during this event. + * @param {Object} event.originalEvent + * The original event object. + * @param {Boolean} event.preventDefaultAction + * Set to true to prevent the tracker subscriber from performing its default action (subscriber implementation dependent). Default: false. + * @param {Object} event.userData + * Arbitrary user-defined object. + */ + keyUpHandler: 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 {Number} event.keyCode + * The key code that was pressed. + * @param {Boolean} event.ctrl + * True if the ctrl key was pressed during this event. + * @param {Boolean} event.shift + * True if the shift key was pressed during this event. + * @param {Boolean} event.alt + * True if the alt key was pressed during this event. + * @param {Boolean} event.meta + * True if the meta key was pressed during this event. * @param {Object} event.originalEvent * The original event object. * @param {Boolean} event.preventDefaultAction @@ -871,7 +970,7 @@ /** * Detect browser pointer device event model(s) and build appropriate list of events to subscribe to. */ - $.MouseTracker.subscribeEvents = [ "click", "dblclick", "keypress", "focus", "blur", $.MouseTracker.wheelEventName ]; + $.MouseTracker.subscribeEvents = [ "click", "dblclick", "keydown", "keyup", "keypress", "focus", "blur", $.MouseTracker.wheelEventName ]; if( $.MouseTracker.wheelEventName == "DOMMouseScroll" ) { // Older Firefox @@ -888,7 +987,6 @@ } else { $.MouseTracker.maxTouchPoints = 0; } - $.MouseTracker.haveTouchEnter = false; $.MouseTracker.haveMouseEnter = false; } else if ( window.MSPointerEvent ) { // IE10 @@ -900,7 +998,6 @@ } else { $.MouseTracker.maxTouchPoints = 0; } - $.MouseTracker.haveTouchEnter = false; $.MouseTracker.haveMouseEnter = false; } else { // Legacy W3C mouse events @@ -914,19 +1011,14 @@ } $.MouseTracker.subscribeEvents.push( "mousedown", "mouseup", "mousemove" ); if ( 'ontouchstart' in window ) { - // iOS, Android, and other W3c Touch Event implementations (see http://www.w3.org/TR/2011/WD-touch-events-20110505) + // iOS, Android, and other W3c Touch Event implementations + // (see http://www.w3.org/TR/touch-events/) + // (see https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html) + // (see https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html) $.MouseTracker.subscribeEvents.push( "touchstart", "touchend", "touchmove", "touchcancel" ); - if ( 'ontouchenter' in window ) { - $.MouseTracker.subscribeEvents.push( "touchenter", "touchleave" ); - $.MouseTracker.haveTouchEnter = true; - } else { - $.MouseTracker.haveTouchEnter = false; - } - } else { - $.MouseTracker.haveTouchEnter = false; } if ( 'ongesturestart' in window ) { - // iOS (see https://developer.apple.com/library/safari/documentation/UserExperience/Reference/GestureEventClassReference/GestureEvent/GestureEvent.html) + // iOS (see https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html) // Subscribe to these to prevent default gesture handling $.MouseTracker.subscribeEvents.push( "gesturestart", "gesturechange" ); } @@ -1414,12 +1506,72 @@ } + /** + * @private + * @inner + */ + function onKeyDown( tracker, event ) { + //$.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, + position: getMouseRelative( event, tracker.element ), + keyCode: event.keyCode ? event.keyCode : event.charCode, + ctrl: event.ctrlKey, + shift: event.shiftKey, + alt: event.altKey, + meta: event.metaKey, + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( !propagate ) { + $.cancelEvent( event ); + } + } + } + + + /** + * @private + * @inner + */ + function onKeyUp( tracker, event ) { + //$.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, + position: getMouseRelative( event, tracker.element ), + keyCode: event.keyCode ? event.keyCode : event.charCode, + ctrl: event.ctrlKey, + shift: event.shiftKey, + alt: event.altKey, + meta: event.metaKey, + originalEvent: event, + preventDefaultAction: false, + userData: tracker.userData + } + ); + if ( !propagate ) { + $.cancelEvent( event ); + } + } + } + + /** * @private * @inner */ function onKeyPress( tracker, event ) { - //console.log( "keypress %s %s %s %s %s", event.keyCode, event.charCode, event.ctrlKey, event.shiftKey, event.altKey ); + //$.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 ); @@ -1428,7 +1580,10 @@ eventSource: tracker, position: getMouseRelative( event, tracker.element ), keyCode: event.keyCode ? event.keyCode : event.charCode, + ctrl: event.ctrlKey, shift: event.shiftKey, + alt: event.altKey, + meta: event.metaKey, originalEvent: event, preventDefaultAction: false, userData: tracker.userData @@ -1609,7 +1764,7 @@ function onMouseOver( tracker, event ) { event = $.getEvent( event ); - if ( this === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { + if ( event.currentTarget === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { return; } @@ -1654,7 +1809,7 @@ function onMouseOut( tracker, event ) { event = $.getEvent( event ); - if ( this === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { + if ( event.currentTarget === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { return; } @@ -1823,45 +1978,24 @@ * @private * @inner */ - function onTouchEnter( tracker, event ) { + function abortTouchContacts( tracker, event, pointsList ) { var i, - touchCount = event.changedTouches.length, - gPoints = []; + gPointCount = pointsList.getLength(), + abortGPoints = []; - for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { - id: event.changedTouches[ i ].identifier, - type: 'touch', - // isPrimary not set - let the updatePointers functions determine it - currentPos: getMouseAbsolute( event.changedTouches[ i ] ), - currentTime: $.now() - } ); + for ( i = 0; i < gPointCount; i++ ) { + abortGPoints.push( pointsList.getByIndex( i ) ); } - updatePointersEnter( tracker, event, gPoints ); - } - - - /** - * @private - * @inner - */ - function onTouchLeave( tracker, event ) { - var i, - touchCount = event.changedTouches.length, - gPoints = []; - - for ( i = 0; i < touchCount; i++ ) { - gPoints.push( { - id: event.changedTouches[ i ].identifier, - type: 'touch', - // isPrimary not set - let the updatePointers functions determine it - currentPos: getMouseAbsolute( event.changedTouches[ i ] ), - currentTime: $.now() - } ); + if ( abortGPoints.length > 0 ) { + // simulate touchend + updatePointersUp( tracker, event, abortGPoints, 0 ); // 0 means primary button press/release or touch contact + // release pointer capture + pointsList.captureCount = 1; + releasePointer( tracker, 'touch' ); + // simulate touchleave + updatePointersExit( tracker, event, abortGPoints ); } - - updatePointersExit( tracker, event, gPoints ); } @@ -1872,11 +2006,19 @@ function onTouchStart( tracker, event ) { var time, i, + j, touchCount = event.changedTouches.length, - gPoints = []; + gPoints = [], + parentGPoints, + pointsList = tracker.getActivePointersListByType( 'touch' ); time = $.now(); + if ( pointsList.getLength() > event.touches.length - touchCount ) { + $.console.warn('Tracked touch contact count doesn\'t match event.touches.length. Removing all tracked touch pointers.'); + abortTouchContacts( tracker, event, pointsList ); + } + for ( i = 0; i < touchCount; i++ ) { gPoints.push( { id: event.changedTouches[ i ].identifier, @@ -1887,9 +2029,24 @@ } ); } - // simulate touchenter if not natively available - if ( !$.MouseTracker.haveTouchEnter ) { - updatePointersEnter( tracker, event, gPoints ); + // 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 ( updatePointersDown( tracker, event, gPoints, 0 ) ) { // 0 means primary button press/release or touch contact @@ -1930,8 +2087,10 @@ function handleTouchEnd( tracker, event ) { var time, i, + j, touchCount = event.changedTouches.length, - gPoints = []; + gPoints = [], + parentGPoints; time = $.now(); @@ -1949,9 +2108,24 @@ releasePointer( tracker, 'touch' ); } - // simulate touchleave if not natively available - if ( !$.MouseTracker.haveTouchEnter && touchCount > 0 ) { - updatePointersExit( tracker, event, gPoints ); + // 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 ); + } } $.cancelEvent( event ); @@ -2054,7 +2228,7 @@ function onPointerOver( tracker, event ) { var gPoint; - if ( this === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { + if ( event.currentTarget === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { return; } @@ -2077,7 +2251,7 @@ function onPointerOut( tracker, event ) { var gPoint; - if ( this === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { + if ( event.currentTarget === event.relatedTarget || isParentChild( event.currentTarget, event.relatedTarget ) ) { return; } @@ -2092,6 +2266,7 @@ updatePointersExit( tracker, event, [ gPoint ] ); } + /** * @private * @inner @@ -2344,6 +2519,7 @@ 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', @@ -2407,6 +2583,7 @@ pointerType: curGPoint.type, position: getPointRelativeToAbsolute( curGPoint.currentPos, tracker.element ), buttons: pointsList.buttons, + pointers: tracker.getActivePointerCount(), insideElementPressed: updateGPoint ? updateGPoint.insideElementPressed : false, buttonDownAny: pointsList.buttons !== 0, isTouchEvent: curGPoint.type === 'touch', diff --git a/src/referencestrip.js b/src/referencestrip.js index 0c0fdf61..668e11eb 100644 --- a/src/referencestrip.js +++ b/src/referencestrip.js @@ -125,6 +125,7 @@ $.ReferenceStrip = function ( options ) { scrollHandler: $.delegate( this, onStripScroll ), enterHandler: $.delegate( this, onStripEnter ), exitHandler: $.delegate( this, onStripExit ), + keyDownHandler: $.delegate( this, onKeyDown ), keyHandler: $.delegate( this, onKeyPress ) } ).setTracking( true ); @@ -518,6 +519,37 @@ function onStripExit( event ) { } +/** + * @private + * @inner + * @function + */ +function onKeyDown( event ) { + //console.log( event.keyCode ); + + if ( !event.preventDefaultAction && !event.ctrl && !event.alt && !event.meta ) { + switch ( event.keyCode ) { + case 38: //up arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + return false; + case 40: //down arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + return false; + case 37: //left arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + return false; + case 39: //right arrow + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + return false; + default: + //console.log( 'navigator keycode %s', event.keyCode ); + return true; + } + } else { + return true; + } +} + /** * @private @@ -527,35 +559,35 @@ function onStripExit( event ) { function onKeyPress( event ) { //console.log( event.keyCode ); - switch ( event.keyCode ) { - case 61: //=|+ - onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); - return false; - case 45: //-|_ - onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); - return false; - case 48: //0|) - case 119: //w - case 87: //W - case 38: //up arrow - onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); - return false; - case 115: //s - case 83: //S - case 40: //down arrow - onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); - return false; - case 97: //a - case 37: //left arrow - onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); - return false; - case 100: //d - case 39: //right arrow - onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); - return false; - default: - //console.log( 'navigator keycode %s', event.keyCode ); - return true; + if ( !event.preventDefaultAction && !event.ctrl && !event.alt && !event.meta ) { + switch ( event.keyCode ) { + case 61: //=|+ + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + return false; + case 45: //-|_ + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + return false; + case 48: //0|) + case 119: //w + case 87: //W + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + return false; + case 115: //s + case 83: //S + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + return false; + case 97: //a + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: -1, shift: null } ); + return false; + case 100: //d + onStripScroll.call( this, { eventSource: this.tracker, position: null, scroll: 1, shift: null } ); + return false; + default: + //console.log( 'navigator keycode %s', event.keyCode ); + return true; + } + } else { + return true; } } diff --git a/src/viewer.js b/src/viewer.js index 4fee330a..b2b0b5a0 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -106,14 +106,6 @@ $.Viewer = function( options ) { * @memberof OpenSeadragon.Viewer# */ container: null, - /** - * A <textarea> element, the element where keyboard events are handled.

- * Child element of {@link OpenSeadragon.Viewer#container}, - * positioned below {@link OpenSeadragon.Viewer#canvas}. - * @member {Element} keyboardCommandArea - * @memberof OpenSeadragon.Viewer# - */ - keyboardCommandArea: null, /** * A <div> element, the element where user-input events are handled for panning and zooming.

* Child element of {@link OpenSeadragon.Viewer#container}, @@ -229,7 +221,6 @@ $.Viewer = function( options ) { this.element = this.element || document.getElementById( this.id ); this.canvas = $.makeNeutralElement( "div" ); - this.keyboardCommandArea = $.makeNeutralElement( "textarea" ); this.canvas.className = "openseadragon-canvas"; (function( style ){ @@ -241,6 +232,7 @@ $.Viewer = function( options ) { style.left = "0px"; }(this.canvas.style)); $.setElementTouchActionNone( this.canvas ); + this.canvas.tabIndex = 0; //the container is created through applying the ControlDock constructor above this.container.className = "openseadragon-container"; @@ -254,19 +246,7 @@ $.Viewer = function( options ) { style.textAlign = "left"; // needed to protect against }( this.container.style )); - this.keyboardCommandArea.className = "keyboard-command-area"; - (function( style ){ - style.width = "100%"; - style.height = "100%"; - style.overflow = "hidden"; - style.position = "absolute"; - style.top = "0px"; - style.left = "0px"; - style.resize = "none"; - }( this.keyboardCommandArea.style )); - this.container.insertBefore( this.canvas, this.container.firstChild ); - this.container.insertBefore( this.keyboardCommandArea, this.container.firstChild ); this.element.appendChild( this.container ); //Used for toggling between fullscreen and default container size @@ -277,80 +257,22 @@ $.Viewer = function( options ) { this.bodyOverflow = document.body.style.overflow; this.docOverflow = document.documentElement.style.overflow; - this.keyboardCommandArea.innerTracker = new $.MouseTracker({ - _this : this, - element: this.keyboardCommandArea, - focusHandler: function( event ){ - if ( !event.preventDefaultAction ) { - var point = $.getElementPosition( this.element ); - window.scrollTo( 0, point.y ); - } - }, - - keyHandler: function( event ){ - if ( !event.preventDefaultAction ) { - switch( event.keyCode ){ - case 61://=|+ - _this.viewport.zoomBy(1.1); - _this.viewport.applyConstraints(); - return false; - case 45://-|_ - _this.viewport.zoomBy(0.9); - _this.viewport.applyConstraints(); - return false; - case 48://0|) - _this.viewport.goHome(); - _this.viewport.applyConstraints(); - return false; - case 119://w - case 87://W - case 38://up arrow - if ( event.shift ) { - _this.viewport.zoomBy(1.1); - } else { - _this.viewport.panBy(new $.Point(0, -0.05)); - } - _this.viewport.applyConstraints(); - return false; - case 115://s - case 83://S - case 40://down arrow - if ( event.shift ) { - _this.viewport.zoomBy(0.9); - } else { - _this.viewport.panBy(new $.Point(0, 0.05)); - } - _this.viewport.applyConstraints(); - return false; - case 97://a - case 37://left arrow - _this.viewport.panBy(new $.Point(-0.05, 0)); - _this.viewport.applyConstraints(); - return false; - case 100://d - case 39://right arrow - _this.viewport.panBy(new $.Point(0.05, 0)); - _this.viewport.applyConstraints(); - return false; - default: - //console.log( 'navigator keycode %s', event.keyCode ); - return true; - } - } - } - }).setTracking( true ); // default state - - this.innerTracker = new $.MouseTracker({ element: this.canvas, clickTimeThreshold: this.clickTimeThreshold, clickDistThreshold: this.clickDistThreshold, dblClickTimeThreshold: this.dblClickTimeThreshold, dblClickDistThreshold: this.dblClickDistThreshold, + focusHandler: $.delegate( this, onCanvasFocus ), + keyDownHandler: $.delegate( this, onCanvasKeyDown ), + keyHandler: $.delegate( this, onCanvasKeyPress ), clickHandler: $.delegate( this, onCanvasClick ), dblClickHandler: $.delegate( this, onCanvasDblClick ), dragHandler: $.delegate( this, onCanvasDrag ), dragEndHandler: $.delegate( this, onCanvasDragEnd ), + enterHandler: $.delegate( this, onCanvasEnter ), + exitHandler: $.delegate( this, onCanvasExit ), + pressHandler: $.delegate( this, onCanvasPress ), releaseHandler: $.delegate( this, onCanvasRelease ), nonPrimaryPressHandler: $.delegate( this, onCanvasNonPrimaryPress ), nonPrimaryReleaseHandler: $.delegate( this, onCanvasNonPrimaryRelease ), @@ -365,9 +287,7 @@ $.Viewer = function( options ) { dblClickTimeThreshold: this.dblClickTimeThreshold, dblClickDistThreshold: this.dblClickDistThreshold, enterHandler: $.delegate( this, onContainerEnter ), - exitHandler: $.delegate( this, onContainerExit ), - pressHandler: $.delegate( this, onContainerPress ), - releaseHandler: $.delegate( this, onContainerRelease ) + exitHandler: $.delegate( this, onContainerExit ) }).setTracking( this.mouseNavEnabled ? true : false ); // always tracking if( this.toolbar ){ @@ -814,9 +734,6 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } // destroy the mouse trackers - if (this.keyboardCommandArea){ - this.keyboardCommandArea.innerTracker.destroy(); - } if (this.innerTracker){ this.innerTracker.destroy(); } @@ -829,7 +746,6 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, // clear all our references to dom objects this.canvas = null; - this.keyboardCommandArea = null; this.container = null; // clear our reference to the main element - they will need to pass it in again, creating a new viewer @@ -2269,9 +2185,109 @@ function onBlur(){ } +function onCanvasFocus( event ) { + if ( !event.preventDefaultAction ) { + var point = $.getElementPosition( this.element ); + window.scrollTo( 0, point.y ); + } +} + +function onCanvasKeyDown( event ) { + if ( !event.preventDefaultAction && !event.ctrl && !event.alt && !event.meta ) { + switch( event.keyCode ){ + case 38://up arrow + if ( event.shift ) { + this.viewport.zoomBy(1.1); + } else { + this.viewport.panBy(new $.Point(0, -0.05)); + } + this.viewport.applyConstraints(); + return false; + case 40://down arrow + if ( event.shift ) { + this.viewport.zoomBy(0.9); + } else { + this.viewport.panBy(new $.Point(0, 0.05)); + } + this.viewport.applyConstraints(); + return false; + case 37://left arrow + this.viewport.panBy(new $.Point(-0.05, 0)); + this.viewport.applyConstraints(); + return false; + case 39://right arrow + this.viewport.panBy(new $.Point(0.05, 0)); + this.viewport.applyConstraints(); + return false; + default: + //console.log( 'navigator keycode %s', event.keyCode ); + return true; + } + } else { + return true; + } +} + +function onCanvasKeyPress( event ) { + if ( !event.preventDefaultAction && !event.ctrl && !event.alt && !event.meta ) { + switch( event.keyCode ){ + case 61://=|+ + this.viewport.zoomBy(1.1); + this.viewport.applyConstraints(); + return false; + case 45://-|_ + this.viewport.zoomBy(0.9); + this.viewport.applyConstraints(); + return false; + case 48://0|) + this.viewport.goHome(); + this.viewport.applyConstraints(); + return false; + case 119://w + case 87://W + if ( event.shift ) { + this.viewport.zoomBy(1.1); + } else { + this.viewport.panBy(new $.Point(0, -0.05)); + } + this.viewport.applyConstraints(); + return false; + case 115://s + case 83://S + if ( event.shift ) { + this.viewport.zoomBy(0.9); + } else { + this.viewport.panBy(new $.Point(0, 0.05)); + } + this.viewport.applyConstraints(); + return false; + case 97://a + this.viewport.panBy(new $.Point(-0.05, 0)); + this.viewport.applyConstraints(); + return false; + case 100://d + this.viewport.panBy(new $.Point(0.05, 0)); + this.viewport.applyConstraints(); + return false; + default: + //console.log( 'navigator keycode %s', event.keyCode ); + return true; + } + } else { + return true; + } +} + function onCanvasClick( event ) { var gestureSettings; + var haveKeyboardFocus = document.activeElement == this.canvas; + + // If we don't have keyboard focus, request it. + if ( !haveKeyboardFocus ) { + this.canvas.focus(); + } + if ( !event.preventDefaultAction && this.viewport && event.quick ) { gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); if ( gestureSettings.clickToZoom ) { @@ -2427,6 +2443,92 @@ function onCanvasDragEnd( event ) { }); } +function onCanvasEnter( event ) { + /** + * Raised when a pointer enters the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-enter + * @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 {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. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-enter', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + buttons: event.buttons, + pointers: event.pointers, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + +function onCanvasExit( event ) { + /** + * Raised when a pointer leaves the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-exit + * @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 {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. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-exit', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + buttons: event.buttons, + pointers: event.pointers, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + +function onCanvasPress( event ) { + /** + * Raised when the primary mouse button is pressed or touch starts on the {@link OpenSeadragon.Viewer#canvas} element. + * + * @event canvas-press + * @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 {String} pointerType - "mouse", "touch", "pen", etc. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'canvas-press', { + tracker: event.eventSource, + pointerType: event.pointerType, + position: event.position, + insideElementPressed: event.insideElementPressed, + insideElementReleased: event.insideElementReleased, + originalEvent: event.originalEvent + }); +} + function onCanvasRelease( event ) { /** * Raised when the primary mouse button is released or touch ends on the {@link OpenSeadragon.Viewer#canvas} element. @@ -2436,6 +2538,7 @@ function onCanvasRelease( 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 {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. * @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released. @@ -2444,6 +2547,7 @@ function onCanvasRelease( event ) { */ this.raiseEvent( 'canvas-release', { tracker: event.eventSource, + pointerType: event.pointerType, position: event.position, insideElementPressed: event.insideElementPressed, insideElementReleased: event.insideElementReleased, @@ -2609,8 +2713,38 @@ function onCanvasScroll( event ) { return false; } +function onContainerEnter( event ) { + THIS[ this.hash ].mouseInside = true; + abortControlsAutoHide( this ); + /** + * Raised when the cursor enters the {@link OpenSeadragon.Viewer#container} element. + * + * @event container-enter + * @memberof OpenSeadragon.Viewer + * @type {object} + * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. + * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. + * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. + * @property {Number} 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. + * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. + * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. + * @property {Object} originalEvent - The original DOM event. + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + this.raiseEvent( 'container-enter', { + tracker: event.eventSource, + position: event.position, + buttons: event.buttons, + pointers: event.pointers, + insideElementPressed: event.insideElementPressed, + buttonDownAny: event.buttonDownAny, + originalEvent: event.originalEvent + }); +} + function onContainerExit( event ) { - if ( !event.insideElementPressed ) { + if ( event.pointers < 1 ) { THIS[ this.hash ].mouseInside = false; if ( !THIS[ this.hash ].animating ) { beginControlsAutoHide( this ); @@ -2626,6 +2760,7 @@ function onContainerExit( event ) { * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. + * @property {Number} pointers - Number of pointers (all types) active in the tracked element. * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. * @property {Object} originalEvent - The original DOM event. @@ -2635,71 +2770,7 @@ function onContainerExit( event ) { tracker: event.eventSource, position: event.position, buttons: event.buttons, - insideElementPressed: event.insideElementPressed, - buttonDownAny: event.buttonDownAny, - originalEvent: event.originalEvent - }); -} - -function onContainerPress( event ) { - if ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) { - THIS[ this.hash ].mouseInside = true; - abortControlsAutoHide( this ); - } -} - -function onContainerRelease( event ) { - if ( !event.insideElementReleased || ( event.pointerType === 'touch' && !$.MouseTracker.haveTouchEnter ) ) { - THIS[ this.hash ].mouseInside = false; - if ( !THIS[ this.hash ].animating ) { - beginControlsAutoHide( this ); - } - } - /** - * Raised when the primary mouse button is released or touch ends on the {@link OpenSeadragon.Viewer#container} element. - * - * @event container-release - * @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 {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. - * @property {Boolean} insideElementReleased - True if the cursor still inside the tracked element when the button was released. - * @property {Object} originalEvent - The original DOM event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.raiseEvent( 'container-release', { - tracker: event.eventSource, - position: event.position, - insideElementPressed: event.insideElementPressed, - insideElementReleased: event.insideElementReleased, - originalEvent: event.originalEvent - }); -} - -function onContainerEnter( event ) { - THIS[ this.hash ].mouseInside = true; - abortControlsAutoHide( this ); - /** - * Raised when the cursor enters the {@link OpenSeadragon.Viewer#container} element. - * - * @event container-enter - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised this event. - * @property {OpenSeadragon.MouseTracker} tracker - A reference to the MouseTracker which originated this event. - * @property {OpenSeadragon.Point} position - The position of the event relative to the tracked element. - * @property {Number} buttons - Current buttons pressed. A combination of bit flags 0: none, 1: primary (or touch contact), 2: secondary, 4: aux (often middle), 8: X1 (often back), 16: X2 (often forward), 32: pen eraser. - * @property {Boolean} insideElementPressed - True if the left mouse button is currently being pressed and was initiated inside the tracked element, otherwise false. - * @property {Boolean} buttonDownAny - Was the button down anywhere in the screen during the event. Deprecated. Use buttons instead. - * @property {Object} originalEvent - The original DOM event. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.raiseEvent( 'container-enter', { - tracker: event.eventSource, - position: event.position, - buttons: event.buttons, + pointers: event.pointers, insideElementPressed: event.insideElementPressed, buttonDownAny: event.buttonDownAny, originalEvent: event.originalEvent diff --git a/test/helpers/legacy.mouse.shim.js b/test/helpers/legacy.mouse.shim.js index 535f8815..3609ed85 100644 --- a/test/helpers/legacy.mouse.shim.js +++ b/test/helpers/legacy.mouse.shim.js @@ -21,19 +21,14 @@ } $.MouseTracker.subscribeEvents.push( "mousedown", "mouseup", "mousemove" ); if ( 'ontouchstart' in window ) { - // iOS, Android, and other W3c Touch Event implementations (see http://www.w3.org/TR/2011/WD-touch-events-20110505) + // iOS, Android, and other W3c Touch Event implementations + // (see http://www.w3.org/TR/touch-events/) + // (see https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html) + // (see https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html) $.MouseTracker.subscribeEvents.push( "touchstart", "touchend", "touchmove", "touchcancel" ); - if ( 'ontouchenter' in window ) { - $.MouseTracker.subscribeEvents.push( "touchenter", "touchleave" ); - $.MouseTracker.haveTouchEnter = true; - } else { - $.MouseTracker.haveTouchEnter = false; - } - } else { - $.MouseTracker.haveTouchEnter = false; } if ( 'ongesturestart' in window ) { - // iOS (see https://developer.apple.com/library/safari/documentation/UserExperience/Reference/GestureEventClassReference/GestureEvent/GestureEvent.html) + // iOS (see https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html) // Subscribe to these to prevent default gesture handling $.MouseTracker.subscribeEvents.push( "gesturestart", "gesturechange" ); }