From 1739294700330a357c0ed823ea91d05cb38569f0 Mon Sep 17 00:00:00 2001 From: thatcher Date: Thu, 2 Feb 2012 19:12:45 -0500 Subject: [PATCH] Cleaned up more of MouseTracker making properties intended to be private, private, and similarly with methods. saved documentation on meaning of private properties and methods where it was already done. MouseTracker api is proably clean enough to try to add touch screen event support now. --- openseadragon.js | 1288 ++++++++++++++++++++++++++----------------- src/mousetracker.js | 1252 +++++++++++++++++++++++++---------------- src/viewer.js | 36 +- 3 files changed, 1576 insertions(+), 1000 deletions(-) diff --git a/openseadragon.js b/openseadragon.js index 97e84cbb..7021df6b 100644 --- a/openseadragon.js +++ b/openseadragon.js @@ -1269,538 +1269,828 @@ $.EventHandler.prototype = { }( OpenSeadragon )); (function( $ ){ - - //Ensures we dont break existing instances of mousetracker if we are dumb - //enough to load openseadragon.js onto the page twice. I don't know how - //useful this pattern is, but if we decide to use it we should use it - //everywhere - if ( $.MouseTracker ) { - return; - } - - var buttonDownAny = false, - ieCapturingAny = false, - ieTrackersActive = {}, // dictionary from hash to MouseTracker - ieTrackersCapturing = []; // list of trackers interested in capture + + // is any button currently being pressed while mouse events occur + var IS_BUTTON_DOWN = false, + // is any tracker currently capturing? + IS_CAPTURING = false, + // dictionary from hash to MouseTracker + ACTIVE = {}, + // list of trackers interested in capture + CAPTURING = [], + // dictionary from hash to private properties + THIS = {}; /** + * The MouseTracker allows other classes to set handlers for common mouse + * events on a specific element like, 'enter', 'exit', 'press', 'release', + * 'scroll', 'click', and 'drag'. * @class + * @param {Object} options + * Allows configurable properties to be entirely specified by passing + * an options object to the constructor. The constructor also supports + * the original positional arguments 'elements', 'clickTimeThreshold', + * and 'clickDistThreshold' in that order. + * @param {Element|String} options.element + * A reference to an element or an element id for which the mouse + * events will be monitored. + * @param {Number} options.clickTimeThreshold + * The number of milliseconds within which mutliple mouse clicks + * will be treated as a single event. + * @param {Number} options.clickDistThreshold + * The distance between mouse click within multiple mouse clicks + * will be treated as a single event. + * @param {Function} options.enterHandler + * An optional handler for mouse enter. + * @param {Function} options.exitHandler + * An optional handler for mouse exit. + * @param {Function} options.pressHandler + * An optional handler for mouse press. + * @param {Function} options.releaseHandler + * An optional handler for mouse release. + * @param {Function} options.scrollHandler + * An optional handler for mouse scroll. + * @param {Function} options.clickHandler + * An optional handler for mouse click. + * @param {Function} options.dragHandler + * An optional handler for mouse drag. + * @property {Number} hash + * An unique hash for this tracker. + * @property {Element} element + * The element for which mouse event are being monitored. + * @property {Number} clickTimeThreshold + * The number of milliseconds within which mutliple mouse clicks + * will be treated as a single event. + * @property {Number} clickDistThreshold + * The distance between mouse click within multiple mouse clicks + * will be treated as a single event. */ - $.MouseTracker = function ( element, clickTimeThreshold, clickDistThreshold ) { - //Start Thatcher - TODO: remove local function definitions in favor of - // - a global closure for MouseTracker so the number - // - of Viewers has less memory impact. Also use - // - prototype pattern instead of Singleton pattern. - //End Thatcher + $.MouseTracker = function ( options ) { - this.hash = Math.random(); // a unique hash for this tracker - this.element = $.getElement( element ); + var args = arguments; - this.tracking = false; - this.capturing = false; - this.buttonDownElement = false; - this.insideElement = false; + if( !$.isPlainObject( options ) ){ + options = { + element: args[ 0 ], + clickTimeThreshold: args[ 1 ], + clickDistThreshold: args[ 2 ] + }; + } - this.lastPoint = null; // position of last mouse down/move - this.lastMouseDownTime = null; // time of last mouse down - this.lastMouseDownPoint = null; // position of last mouse down - this.clickTimeThreshold = clickTimeThreshold; - this.clickDistThreshold = clickDistThreshold; + this.hash = Math.random(); + this.element = $.getElement( options.element ); + this.clickTimeThreshold = options.clickTimeThreshold; + this.clickDistThreshold = options.clickDistThreshold; - this.target = element; - this.enterHandler = null; // function(tracker, position, buttonDownElement, buttonDownAny) - this.exitHandler = null; // function(tracker, position, buttonDownElement, buttonDownAny) - this.pressHandler = null; // function(tracker, position) - this.releaseHandler = null; // function(tracker, position, insideElementPress, insideElementRelease) - this.scrollHandler = null; // function(tracker, position, scroll, shift) - this.clickHandler = null; // function(tracker, position, quick, shift) - this.dragHandler = null; // function(tracker, position, delta, shift) + this.enterHandler = options.enterHandler || null; + this.exitHandler = options.exitHandler || null; + this.pressHandler = options.pressHandler || null; + this.releaseHandler = options.releaseHandler || null; + this.scrollHandler = options.scrollHandler || null; + this.clickHandler = options.clickHandler || null; + this.dragHandler = options.dragHandler || null; - this.delegates = { - "mouseover": $.delegate(this, this.onMouseOver), - "mouseout": $.delegate(this, this.onMouseOut), - "mousedown": $.delegate(this, this.onMouseDown), - "mouseup": $.delegate(this, this.onMouseUp), - "click": $.delegate(this, this.onMouseClick), - "DOMMouseScroll": $.delegate(this, this.onMouseWheelSpin), - "mousewheel": $.delegate(this, this.onMouseWheelSpin), - "mouseupie": $.delegate(this, this.onMouseUpIE), - "mousemoveie": $.delegate(this, this.onMouseMoveIE), - "mouseupwindow": $.delegate(this, this.onMouseUpWindow), - "mousemove": $.delegate(this, this.onMouseMove) + //Store private properties in a scope sealed hash map + var _this = this; + + /** + * @private + * @property {Boolean} tracking + * Are we currently tracking mouse events. + * @property {Boolean} capturing + * Are we curruently capturing mouse events. + * @property {Boolean} buttonDown + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @property {Boolean} insideElement + * Are we currently inside the screen area of the tracked element. + * @property {OpenSeadragon.Point} lastPoint + * Position of last mouse down/move + * @property {Number} lastMouseDownTime + * Time of last mouse down. + * @property {OpenSeadragon.Point} lastMouseDownPoint + * Position of last mouse down + */ + THIS[ this.hash ] = { + "mouseover": function( event ){ onMouseOver( _this, event ); }, + "mouseout": function( event ){ onMouseOut( _this, event ); }, + "mousedown": function( event ){ onMouseDown( _this, event ); }, + "mouseup": function( event ){ onMouseUp( _this, event ); }, + "click": function( event ){ onMouseClick( _this, event ); }, + "DOMMouseScroll": function( event ){ onMouseWheelSpin( _this, event ); }, + "mousewheel": function( event ){ onMouseWheelSpin( _this, event ); }, + "mouseupie": function( event ){ onMouseUpIE( _this, event ); }, + "mousemoveie": function( event ){ onMouseMoveIE( _this, event ); }, + "mouseupwindow": function( event ){ onMouseUpWindow( _this, event ); }, + "mousemove": function( event ){ onMouseMove( _this, event ); }, + tracking : false, + capturing : false, + buttonDown : false, + insideElement : false, + lastPoint : null, + lastMouseDownTime : null, + lastMouseDownPoint : null }; }; $.MouseTracker.prototype = { - /** - * @method + * Are we currently tracking events on this element. + * @deprecated Just use this.tracking + * @function + * @returns {Boolean} Are we currently tracking events on this element. */ isTracking: function () { - return this.tracking; + return THIS[ this.hash ].tracking; }, /** - * @method + * Enable or disable whether or not we are tracking events on this element. + * @function + * @param {Boolean} track True to start tracking, false to stop tracking. + * @returns {OpenSeadragon.MouseTracker} Chainable. */ setTracking: function ( track ) { if ( track ) { - this.startTracking(); + startTracking( this ); } else { - this.stopTracking(); + stopTracking( this ); } + //chain + return this; }, - - /** - * @method - */ - startTracking: function() { - if ( !this.tracking ) { - $.addEvent( this.element, "mouseover", this.delegates["mouseover"], false); - $.addEvent( this.element, "mouseout", this.delegates["mouseout"], false); - $.addEvent( this.element, "mousedown", this.delegates["mousedown"], false); - $.addEvent( this.element, "mouseup", this.delegates["mouseup"], false); - $.addEvent( this.element, "click", this.delegates["click"], false); - $.addEvent( this.element, "DOMMouseScroll", this.delegates["DOMMouseScroll"], false); - $.addEvent( this.element, "mousewheel", this.delegates["mousewheel"], false); // Firefox - - this.tracking = true; - ieTrackersActive[ this.hash ] = this; - } - }, - - /** - * @method - */ - stopTracking: function() { - if ( this.tracking ) { - $.removeEvent( this.element, "mouseover", this.delegates["mouseover"], false); - $.removeEvent( this.element, "mouseout", this.delegates["mouseout"], false); - $.removeEvent( this.element, "mousedown", this.delegates["mousedown"], false); - $.removeEvent( this.element, "mouseup", this.delegates["mouseup"], false); - $.removeEvent( this.element, "click", this.delegates["click"], false); - $.removeEvent( this.element, "DOMMouseScroll", this.delegates["DOMMouseScroll"], false); - $.removeEvent( this.element, "mousewheel", this.delegates["mousewheel"], false); - - this.releaseMouse(); - this.tracking = false; - delete ieTrackersActive[ this.hash ]; - } - }, - - /** - * @method - */ - captureMouse: function() { - if ( !this.capturing ) { - if ( $.Browser.vendor == $.BROWSERS.IE ) { - $.removeEvent( this.element, "mouseup", this.delegates["mouseup"], false ); - $.addEvent( this.element, "mouseup", this.delegates["mouseupie"], true ); - $.addEvent( this.element, "mousemove", this.delegates["mousemoveie"], true ); - } else { - $.addEvent( window, "mouseup", this.delegates["mouseupwindow"], true ); - $.addEvent( window, "mousemove", this.delegates["mousemove"], true ); - } - - this.capturing = true; - } - }, - /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Boolean} buttonDown + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @param {Boolean} buttonDownAny + * Was the button down anywhere in the screen during the event. */ - releaseMouse: function() { - if ( this.capturing ) { - if ( $.Browser.vendor == $.BROWSERS.IE ) { - $.removeEvent( this.element, "mousemove", this.delegates["mousemoveie"], true ); - $.removeEvent( this.element, "mouseup", this.delegates["mouseupie"], true ); - $.addEvent( this.element, "mouseup", this.delegates["mouseup"], false ); - } else { - $.removeEvent( window, "mousemove", this.delegates["mousemove"], true ); - $.removeEvent( window, "mouseup", this.delegates["mouseupwindow"], true ); - } - - this.capturing = false; - } - }, - + enterHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Boolean} buttonDown + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @param {Boolean} buttonDownAny + * Was the button down anywhere in the screen during the event. */ - triggerOthers: function( eventName, event ) { - var trackers = ieTrackersActive, - otherHash; - for ( otherHash in trackers ) { - if ( trackers.hasOwnProperty( otherHash ) && this.hash != otherHash ) { - trackers[ otherHash ][ eventName ]( event ); - } - } - }, + exitHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. */ - hasMouse: function() { - return this.insideElement; - }, - + pressHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Boolean} buttonDown + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @param {Boolean} insideElementRelease + * Was the mouse still inside the tracked element when the button + * was released. */ - onMouseOver: function( event ) { - var event = $.getEvent( event ); - - if ( $.Browser.vendor == $.BROWSERS.IE && - this.capturing && - !isChild( event.srcElement, this.element ) ) { - this.triggerOthers( "onMouseOver", event ); - } - - var to = event.target ? - event.target : - event.srcElement, - from = event.relatedTarget ? - event.relatedTarget : - event.fromElement; - - if ( !isChild( this.element, to ) || isChild( this.element, from ) ) { - return; - } - - this.insideElement = true; - - if ( typeof( this.enterHandler ) == "function") { - try { - this.enterHandler( - this, - getMouseRelative( event, this.element ), - this.buttonDownElement, - buttonDownAny - ); - } catch ( e ) { - $.console.error( - e.name + " while executing enter handler: " + e.message, - e - ); - } - } - }, + releaseHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Number} scroll + * The scroll delta for the event. + * @param {Boolean} shift + * Was the shift key being pressed during this event? */ - onMouseOut: function( event ) { - var event = $.getEvent( event ); - - if ( $.Browser.vendor == $.BROWSERS.IE && - this.capturing && - !isChild( event.srcElement, this.element ) ) { - this.triggerOthers( "onMouseOut", event ); - } - - var from = event.target ? - event.target : - event.srcElement, - to = event.relatedTarget ? - event.relatedTarget : - event.toElement; - - if ( !isChild( this.element, from ) || isChild( this.element, to ) ) { - return; - } - - this.insideElement = false; - - if ( typeof( this.exitHandler ) == "function" ) { - try { - this.exitHandler( - this, - getMouseRelative( event, this.element ), - this.buttonDownElement, - buttonDownAny - ); - } catch ( e ) { - $.console.error( - e.name + " while executing exit handler: " + e.message, - e - ); - } - } - }, + scrollHandler: function(){}, /** - * @method - * @inner + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Boolean} quick + * True only if the clickDistThreshold and clickDeltaThreshold are + * both pased. Useful for ignoring events. + * @param {Boolean} shift + * Was the shift key being pressed during this event? */ - onMouseDown: function( event ) { - var event = $.getEvent( event ); - - if ( event.button == 2 ) { - return; - } - - this.buttonDownElement = true; - - this.lastPoint = getMouseAbsolute( event ); - this.lastMouseDownPoint = this.lastPoint; - this.lastMouseDownTime = new Date().getTime(); - - if ( typeof( this.pressHandler ) == "function" ) { - try { - this.pressHandler( - this, - getMouseRelative( event, this.element ) - ); - } catch (e) { - $.console.error( - e.name + " while executing press handler: " + e.message, - e - ); - } - } - - if ( this.pressHandler || this.dragHandler ) { - $.cancelEvent( event ); - } - - if ( !( $.Browser.vendor == $.BROWSERS.IE ) || !ieCapturingAny ) { - this.captureMouse(); - ieCapturingAny = true; - ieTrackersCapturing = [ this ]; // reset to empty & add us - } else if ( $.Browser.vendor == $.BROWSERS.IE ) { - ieTrackersCapturing.push( this ); // add us to the list - } - }, + clickHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {OpenSeadragon.Point} delta + * The x,y components of the difference between start drag and + * end drag. Usefule for ignoring or weighting the events. + * @param {Boolean} shift + * Was the shift key being pressed during this event? */ - onMouseUp: function( event ) { - var event = $.getEvent( event ), - insideElementPress = this.buttonDownElement, - insideElementRelease = this.insideElement; - - if ( event.button == 2 ) { - return; - } - - this.buttonDownElement = false; - - if ( typeof( this.releaseHandler ) == "function" ) { - try { - this.releaseHandler( - this, - getMouseRelative( event, this.element ), - insideElementPress, - insideElementRelease - ); - } catch (e) { - $.console.error( - e.name + " while executing release handler: " + e.message, - e - ); - } - } - - if ( insideElementPress && insideElementRelease ) { - this.handleMouseClick( event ); - } - }, - - /** - * @method - * Only triggered once by the deepest element that initially received - * the mouse down event. We want to make sure THIS event doesn't bubble. - * Instead, we want to trigger the elements that initially received the - * mouse down event (including this one) only if the mouse is no longer - * inside them. Then, we want to release capture, and emulate a regular - * mouseup on the event that this event was meant for. - */ - onMouseUpIE: function( event ) { - var event = $.getEvent( event ), - tracker, - i; - - if ( event.button == 2 ) { - return; - } - - for ( i = 0; i < ieTrackersCapturing.length; i++ ) { - tracker = ieTrackersCapturing[ i ]; - if ( !tracker.hasMouse() ) { - tracker.onMouseUp( event ); - } - } - - this.releaseMouse(); - ieCapturingAny = false; - event.srcElement.fireEvent( - "on" + event.type, - document.createEventObject( event ) - ); - - $.stopEvent( event ); - }, - - /** - * @method - * Only triggered in W3C browsers by elements within which the mouse was - * initially pressed, since they are now listening to the window for - * mouseup during the capture phase. We shouldn't handle the mouseup - * here if the mouse is still inside this element, since the regular - * mouseup handler will still fire. - */ - onMouseUpWindow: function( event ) { - if ( !this.insideElement ) { - this.onMouseUp( event ); - } - - this.releaseMouse(); - }, - - /** - * @method - */ - onMouseClick: function( event ) { - - if ( this.clickHandler ) { - $.cancelEvent( event ); - } - }, - - /** - * @method - */ - onMouseWheelSpin: function( event ) { - var nDelta = 0; - - if ( !event ) { // For IE, access the global (window) event object - event = window.event; - } - - if ( event.wheelDelta ) { // IE and Opera - nDelta = event.wheelDelta; - if ( window.opera ) { // Opera has the values reversed - nDelta = -nDelta; - } - } else if (event.detail) { // Mozilla FireFox - nDelta = -event.detail; - } - - nDelta = nDelta > 0 ? 1 : -1; - - if ( typeof( this.scrollHandler ) == "function" ) { - try { - this.scrollHandler( - this, - getMouseRelative( event, this.element ), - nDelta, - event.shiftKey - ); - } catch (e) { - $.console.error( - e.name + " while executing scroll handler: " + e.message, - e - ); - } - - $.cancelEvent( event ); - } - }, - - /** - * @method - */ - handleMouseClick: function( event ) { - var event = $.getEvent( event ); - - if ( event.button == 2 ) { - return; - } - - var time = new Date().getTime() - this.lastMouseDownTime; - var point = getMouseAbsolute( event ); - var distance = this.lastMouseDownPoint.distanceTo( point ); - var quick = ( - time <= this.clickTimeThreshold - ) && ( - distance <= this.clickDistThreshold - ); - - if ( typeof( this.clickHandler ) == "function" ) { - try { - this.clickHandler( - this, - getMouseRelative( event, this.element ), - quick, - event.shiftKey - ); - } catch ( e ) { - $.console.error( - e.name + " while executing click handler: " + e.message, - e - ); - } - } - }, - - /** - * @method - */ - onMouseMove: function( event ) { - var event = $.getEvent( event ); - var point = getMouseAbsolute( event ); - var delta = point.minus( this.lastPoint ); - - this.lastPoint = point; - - if ( typeof( this.dragHandler ) == "function" ) { - try { - this.dragHandler( - this, - getMouseRelative( event, this.element ), - delta, - event.shiftKey - ); - } catch (e) { - $.console.error( - e.name + " while executing drag handler: " + e.message, - e - ); - } - - $.cancelEvent( event ); - } - }, - - /** - * Only triggered once by the deepest element that initially received - * the mouse down event. Since no other element has captured the mouse, - * we want to trigger the elements that initially received the mouse - * down event (including this one). - * @method - */ - onMouseMoveIE: function( event ) { - var i; - for ( i = 0; i < ieTrackersCapturing.length; i++ ) { - ieTrackersCapturing[ i ].onMouseMove( event ); - } - - $.stopEvent( event ); - } + dragHandler: function(){} }; /** - * @private - * @inner - */ + * Starts tracking mouse events on this element. + * @private + * @inner + */ + function startTracking( tracker ) { + var events = [ + "mouseover", "mouseout", "mousedown", "mouseup", "click", + "DOMMouseScroll", "mousewheel" + ], + delegate = THIS[ tracker.hash ], + event, + i; + + if ( !delegate.tracking ) { + for( i = 0; i < events.length; i++ ){ + event = events[ i ]; + $.addEvent( + tracker.element, + event, + delegate[ event ], + false + ); + } + delegate.tracking = true; + ACTIVE[ tracker.hash ] = tracker; + } + }; + + /** + * Stops tracking mouse events on this element. + * @private + * @inner + */ + function stopTracking( tracker ) { + var events = [ + "mouseover", "mouseout", "mousedown", "mouseup", "click", + "DOMMouseScroll", "mousewheel" + ], + delegate = THIS[ tracker.hash ], + event, + i; + + if ( delegate.tracking ) { + for( i = 0; i < events.length; i++ ){ + event = events[ i ]; + $.removeEvent( + tracker.element, + event, + delegate[ event ], + false + ); + } + + releaseMouse( tracker ); + delegate.tracking = false; + delete ACTIVE[ tracker.hash ]; + } + }; + + /** + * @private + * @inner + */ + function hasMouse( tracker ) { + return THIS[ tracker.hash ].insideElement; + }; + + /** + * Begin capturing mouse events on this element. + * @private + * @inner + */ + function captureMouse( tracker ) { + var delegate = THIS[ tracker.hash ]; + if ( !delegate.capturing ) { + + if ( $.Browser.vendor == $.BROWSERS.IE ) { + $.removeEvent( + tracker.element, + "mouseup", + delegate[ "mouseup" ], + false + ); + $.addEvent( + tracker.element, + "mouseup", + delegate[ "mouseupie" ], + true + ); + $.addEvent( + tracker.element, + "mousemove", + delegate[ "mousemoveie" ], + true + ); + } else { + $.addEvent( + window, + "mouseup", + delegate[ "mouseupwindow" ], + true + ); + $.addEvent( + window, + "mousemove", + delegate[ "mousemove" ], + true + ); + } + delegate.capturing = true; + } + }; + + + /** + * Stop capturing mouse events on this element. + * @private + * @inner + */ + function releaseMouse( tracker ) { + var delegate = THIS[ tracker.hash ]; + if ( delegate.capturing ) { + + if ( $.Browser.vendor == $.BROWSERS.IE ) { + $.removeEvent( + tracker.element, + "mousemove", + delegate[ "mousemoveie" ], + true + ); + $.removeEvent( + tracker.element, + "mouseup", + delegate[ "mouseupie" ], + true + ); + $.addEvent( + tracker.element, + "mouseup", + delegate[ "mouseup" ], + false + ); + } else { + $.removeEvent( + window, + "mousemove", + delegate[ "mousemove" ], + true + ); + $.removeEvent( + window, + "mouseup", + delegate[ "mouseupwindow" ], + true + ); + } + delegate.capturing = false; + } + }; + + /** + * @private + * @inner + */ + function triggerOthers( tracker, handler, event ) { + var otherHash; + for ( otherHash in ACTIVE ) { + if ( trackers.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { + handler( ACTIVE[ otherHash ], event ); + } + } + }; + + /** + * @private + * @inner + */ + function onMouseOver( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ]; + + if ( $.Browser.vendor == $.BROWSERS.IE && + delegate.capturing && + !isChild( event.srcElement, tracker.element ) ) { + + triggerOthers( tracker, onMouseOver, event ); + + } + + var to = event.target ? + event.target : + event.srcElement, + from = event.relatedTarget ? + event.relatedTarget : + event.fromElement; + + if ( !isChild( tracker.element, to ) || + isChild( tracker.element, from ) ) { + return; + } + + delegate.insideElement = true; + + if ( tracker.enterHandler ) { + try { + tracker.enterHandler( + tracker, + getMouseRelative( event, tracker.element ), + delegate.buttonDown, + IS_BUTTON_DOWN + ); + } catch ( e ) { + $.console.error( + "%s while executing enter handler: %s", + e.name, + e.message, + e + ); + } + } + }; + + /** + * @private + * @inner + */ + function onMouseOut( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ]; + + if ( $.Browser.vendor == $.BROWSERS.IE && + delegate.capturing && + !isChild( event.srcElement, tracker.element ) ) { + + triggerOthers( tracker, onMouseOut, event ); + + } + + var from = event.target ? + event.target : + event.srcElement, + to = event.relatedTarget ? + event.relatedTarget : + event.toElement; + + if ( !isChild( tracker.element, from ) || + isChild( tracker.element, to ) ) { + return; + } + + delegate.insideElement = false; + + if ( tracker.exitHandler ) { + try { + tracker.exitHandler( + tracker, + getMouseRelative( event, tracker.element ), + delegate.buttonDown, + IS_BUTTON_DOWN + ); + } catch ( e ) { + $.console.error( + "%s while executing exit handler: %s", + e.name, + e.message, + e + ); + } + } + }; + + /** + * @private + * @inner + */ + function onMouseDown( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ]; + + if ( event.button == 2 ) { + return; + } + + delegate.buttonDown = true; + + delegate.lastPoint = getMouseAbsolute( event ); + delegate.lastMouseDownPoint = delegate.lastPoint; + delegate.lastMouseDownTime = +new Date(); + + if ( tracker.pressHandler ) { + try { + tracker.pressHandler( + tracker, + getMouseRelative( event, tracker.element ) + ); + } catch (e) { + $.console.error( + "%s while executing press handler: %s", + e.name, + e.message, + e + ); + } + } + + if ( tracker.pressHandler || tracker.dragHandler ) { + $.cancelEvent( event ); + } + + if ( !( $.Browser.vendor == $.BROWSERS.IE ) || !IS_CAPTURING ) { + captureMouse( tracker ); + IS_CAPTURING = true; + // reset to empty & add us + CAPTURING = [ tracker ]; + } else if ( $.Browser.vendor == $.BROWSERS.IE ) { + // add us to the list + CAPTURING.push( tracker ); + } + }; + + /** + * @private + * @inner + */ + function onMouseUp( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ], + //were we inside the tracked element when we were pressed + insideElementPress = delegate.buttonDown, + //are we still inside the tracked element when we released + insideElementRelease = delegate.insideElement; + + if ( event.button == 2 ) { + return; + } + + delegate.buttonDown = false; + + if ( tracker.releaseHandler ) { + try { + tracker.releaseHandler( + tracker, + getMouseRelative( event, tracker.element ), + insideElementPress, + insideElementRelease + ); + } catch (e) { + $.console.error( + "%s while executing release handler: %s", + e.name, + e.message, + e + ); + } + } + + if ( insideElementPress && insideElementRelease ) { + handleMouseClick( tracker, event ); + } + }; + + /** + * Only triggered once by the deepest element that initially received + * the mouse down event. We want to make sure THIS event doesn't bubble. + * Instead, we want to trigger the elements that initially received the + * mouse down event (including this one) only if the mouse is no longer + * inside them. Then, we want to release capture, and emulate a regular + * mouseup on the event that this event was meant for. + * @private + * @inner + */ + function onMouseUpIE( tracker, event ) { + var event = $.getEvent( event ), + othertracker, + i; + + if ( event.button == 2 ) { + return; + } + + for ( i = 0; i < CAPTURING.length; i++ ) { + othertracker = CAPTURING[ i ]; + if ( !hasMouse( othertracker ) ) { + onMouseUp( othertracker, event ); + } + } + + releaseMouse( tracker ); + IS_CAPTURING = false; + event.srcElement.fireEvent( + "on" + event.type, + document.createEventObject( event ) + ); + + $.stopEvent( event ); + }; + + /** + * Only triggered in W3C browsers by elements within which the mouse was + * initially pressed, since they are now listening to the window for + * mouseup during the capture phase. We shouldn't handle the mouseup + * here if the mouse is still inside this element, since the regular + * mouseup handler will still fire. + * @private + * @inner + */ + function onMouseUpWindow( tracker, event ) { + if ( ! THIS[ tracker.hash ].insideElement ) { + onMouseUp( tracker, event ); + } + releaseMouse( tracker ); + }; + + /** + * @private + * @inner + */ + function onMouseClick( tracker, event ) { + if ( tracker.clickHandler ) { + $.cancelEvent( event ); + } + }; + + /** + * @private + * @inner + */ + function onMouseWheelSpin( tracker, event ) { + var nDelta = 0; + + if ( !event ) { // For IE, access the global (window) event object + event = window.event; + } + + if ( event.wheelDelta ) { // IE and Opera + nDelta = event.wheelDelta; + if ( window.opera ) { // Opera has the values reversed + nDelta = -nDelta; + } + } else if (event.detail) { // Mozilla FireFox + nDelta = -event.detail; + } + + nDelta = nDelta > 0 ? 1 : -1; + + if ( tracker.scrollHandler ) { + try { + tracker.scrollHandler( + tracker, + getMouseRelative( event, tracker.element ), + nDelta, + event.shiftKey + ); + } catch (e) { + $.console.error( + "%s while executing scroll handler: %s", + e.name, + e.message, + e + ); + } + + $.cancelEvent( event ); + } + }; + + /** + * @private + * @inner + */ + function handleMouseClick( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ]; + + if ( event.button == 2 ) { + return; + } + + var time = +new Date() - delegate.lastMouseDownTime, + point = getMouseAbsolute( event ), + distance = delegate.lastMouseDownPoint.distanceTo( point ), + quick = time <= tracker.clickTimeThreshold && + distance <= tracker.clickDistThreshold; + + if ( tracker.clickHandler ) { + try { + tracker.clickHandler( + tracker, + getMouseRelative( event, tracker.element ), + quick, + event.shiftKey + ); + } catch ( e ) { + $.console.error( + "%s while executing click handler: %s", + e.name, + e.message, + e + ); + } + } + }; + + /** + * @private + * @inner + */ + function onMouseMove( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ], + point = getMouseAbsolute( event ), + delta = point.minus( delegate.lastPoint ); + + delegate.lastPoint = point; + + if ( tracker.dragHandler ) { + try { + tracker.dragHandler( + tracker, + getMouseRelative( event, tracker.element ), + delta, + event.shiftKey + ); + } catch (e) { + $.console.error( + "%s while executing drag handler: %s", + e.name, + e.message, + e + ); + } + + $.cancelEvent( event ); + } + }; + + /** + * Only triggered once by the deepest element that initially received + * the mouse down event. Since no other element has captured the mouse, + * we want to trigger the elements that initially received the mouse + * down event (including this one). The the param tracker isn't used + * but for consistency with the other event handlers we include it. + * @private + * @inner + */ + function onMouseMoveIE( tracker, event ) { + var i; + for ( i = 0; i < CAPTURING.length; i++ ) { + onMouseMove( CAPTURING[ i ], event ); + } + + $.stopEvent( event ); + }; + + /** + * @private + * @inner + */ function getMouseAbsolute( event ) { return $.getMousePosition( event ); }; @@ -1838,7 +2128,7 @@ $.EventHandler.prototype = { * @inner */ function onGlobalMouseDown() { - buttonDownAny = true; + IS_BUTTON_DOWN = true; }; /** @@ -1846,7 +2136,7 @@ $.EventHandler.prototype = { * @inner */ function onGlobalMouseUp() { - buttonDownAny = false; + IS_BUTTON_DOWN = false; }; @@ -2109,26 +2399,24 @@ $.Viewer = function( options ) { this._forceRedraw = false; this._mouseInside = false; - this.innerTracker = new $.MouseTracker( - this.canvas, - this.config.clickTimeThreshold, - this.config.clickDistThreshold - ); - this.innerTracker.clickHandler = $.delegate( this, onCanvasClick ); - this.innerTracker.dragHandler = $.delegate( this, onCanvasDrag ); - this.innerTracker.releaseHandler = $.delegate( this, onCanvasRelease ); - this.innerTracker.scrollHandler = $.delegate( this, onCanvasScroll ); - this.innerTracker.setTracking( true ); // default state + this.innerTracker = new $.MouseTracker({ + element: this.canvas, + clickTimeThreshold: this.config.clickTimeThreshold, + clickDistThreshold: this.config.clickDistThreshold, + clickHandler: $.delegate( this, onCanvasClick ), + dragHandler: $.delegate( this, onCanvasDrag ), + releaseHandler: $.delegate( this, onCanvasRelease ), + scrollHandler: $.delegate( this, onCanvasScroll ) + }).setTracking( true ); // default state - this.outerTracker = new $.MouseTracker( - this.container, - this.config.clickTimeThreshold, - this.config.clickDistThreshold - ); - this.outerTracker.enterHandler = $.delegate( this, onContainerEnter ); - this.outerTracker.exitHandler = $.delegate( this, onContainerExit ); - this.outerTracker.releaseHandler = $.delegate( this, onContainerRelease ); - this.outerTracker.setTracking( true ); // always tracking + this.outerTracker = new $.MouseTracker({ + element: this.container, + clickTimeThreshold: this.config.clickTimeThreshold, + clickDistThreshold: this.config.clickDistThreshold, + enterHandler: $.delegate( this, onContainerEnter ), + exitHandler: $.delegate( this, onContainerExit ), + releaseHandler: $.delegate( this, onContainerRelease ) + }).setTracking( true ); // always tracking (function( canvas ){ canvas.width = "100%"; diff --git a/src/mousetracker.js b/src/mousetracker.js index d9dac735..a0109a6f 100644 --- a/src/mousetracker.js +++ b/src/mousetracker.js @@ -1,537 +1,827 @@ (function( $ ){ - - //Ensures we dont break existing instances of mousetracker if we are dumb - //enough to load openseadragon.js onto the page twice. I don't know how - //useful this pattern is, but if we decide to use it we should use it - //everywhere - if ( $.MouseTracker ) { - return; - } - - var buttonDownAny = false, - ieCapturingAny = false, - ieTrackersActive = {}, // dictionary from hash to MouseTracker - ieTrackersCapturing = []; // list of trackers interested in capture + + // is any button currently being pressed while mouse events occur + var IS_BUTTON_DOWN = false, + // is any tracker currently capturing? + IS_CAPTURING = false, + // dictionary from hash to MouseTracker + ACTIVE = {}, + // list of trackers interested in capture + CAPTURING = [], + // dictionary from hash to private properties + THIS = {}; /** + * The MouseTracker allows other classes to set handlers for common mouse + * events on a specific element like, 'enter', 'exit', 'press', 'release', + * 'scroll', 'click', and 'drag'. * @class + * @param {Object} options + * Allows configurable properties to be entirely specified by passing + * an options object to the constructor. The constructor also supports + * the original positional arguments 'elements', 'clickTimeThreshold', + * and 'clickDistThreshold' in that order. + * @param {Element|String} options.element + * A reference to an element or an element id for which the mouse + * events will be monitored. + * @param {Number} options.clickTimeThreshold + * The number of milliseconds within which mutliple mouse clicks + * will be treated as a single event. + * @param {Number} options.clickDistThreshold + * The distance between mouse click within multiple mouse clicks + * will be treated as a single event. + * @param {Function} options.enterHandler + * An optional handler for mouse enter. + * @param {Function} options.exitHandler + * An optional handler for mouse exit. + * @param {Function} options.pressHandler + * An optional handler for mouse press. + * @param {Function} options.releaseHandler + * An optional handler for mouse release. + * @param {Function} options.scrollHandler + * An optional handler for mouse scroll. + * @param {Function} options.clickHandler + * An optional handler for mouse click. + * @param {Function} options.dragHandler + * An optional handler for mouse drag. + * @property {Number} hash + * An unique hash for this tracker. + * @property {Element} element + * The element for which mouse event are being monitored. + * @property {Number} clickTimeThreshold + * The number of milliseconds within which mutliple mouse clicks + * will be treated as a single event. + * @property {Number} clickDistThreshold + * The distance between mouse click within multiple mouse clicks + * will be treated as a single event. */ - $.MouseTracker = function ( element, clickTimeThreshold, clickDistThreshold ) { - //Start Thatcher - TODO: remove local function definitions in favor of - // - a global closure for MouseTracker so the number - // - of Viewers has less memory impact. Also use - // - prototype pattern instead of Singleton pattern. - //End Thatcher + $.MouseTracker = function ( options ) { - this.hash = Math.random(); // a unique hash for this tracker - this.element = $.getElement( element ); + var args = arguments; - this.tracking = false; - this.capturing = false; - this.buttonDownElement = false; - this.insideElement = false; + if( !$.isPlainObject( options ) ){ + options = { + element: args[ 0 ], + clickTimeThreshold: args[ 1 ], + clickDistThreshold: args[ 2 ] + }; + } - this.lastPoint = null; // position of last mouse down/move - this.lastMouseDownTime = null; // time of last mouse down - this.lastMouseDownPoint = null; // position of last mouse down - this.clickTimeThreshold = clickTimeThreshold; - this.clickDistThreshold = clickDistThreshold; + this.hash = Math.random(); + this.element = $.getElement( options.element ); + this.clickTimeThreshold = options.clickTimeThreshold; + this.clickDistThreshold = options.clickDistThreshold; - this.target = element; - this.enterHandler = null; // function(tracker, position, buttonDownElement, buttonDownAny) - this.exitHandler = null; // function(tracker, position, buttonDownElement, buttonDownAny) - this.pressHandler = null; // function(tracker, position) - this.releaseHandler = null; // function(tracker, position, insideElementPress, insideElementRelease) - this.scrollHandler = null; // function(tracker, position, scroll, shift) - this.clickHandler = null; // function(tracker, position, quick, shift) - this.dragHandler = null; // function(tracker, position, delta, shift) + this.enterHandler = options.enterHandler || null; + this.exitHandler = options.exitHandler || null; + this.pressHandler = options.pressHandler || null; + this.releaseHandler = options.releaseHandler || null; + this.scrollHandler = options.scrollHandler || null; + this.clickHandler = options.clickHandler || null; + this.dragHandler = options.dragHandler || null; - this.delegates = { - "mouseover": $.delegate(this, this.onMouseOver), - "mouseout": $.delegate(this, this.onMouseOut), - "mousedown": $.delegate(this, this.onMouseDown), - "mouseup": $.delegate(this, this.onMouseUp), - "click": $.delegate(this, this.onMouseClick), - "DOMMouseScroll": $.delegate(this, this.onMouseWheelSpin), - "mousewheel": $.delegate(this, this.onMouseWheelSpin), - "mouseupie": $.delegate(this, this.onMouseUpIE), - "mousemoveie": $.delegate(this, this.onMouseMoveIE), - "mouseupwindow": $.delegate(this, this.onMouseUpWindow), - "mousemove": $.delegate(this, this.onMouseMove) + //Store private properties in a scope sealed hash map + var _this = this; + + /** + * @private + * @property {Boolean} tracking + * Are we currently tracking mouse events. + * @property {Boolean} capturing + * Are we curruently capturing mouse events. + * @property {Boolean} buttonDown + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @property {Boolean} insideElement + * Are we currently inside the screen area of the tracked element. + * @property {OpenSeadragon.Point} lastPoint + * Position of last mouse down/move + * @property {Number} lastMouseDownTime + * Time of last mouse down. + * @property {OpenSeadragon.Point} lastMouseDownPoint + * Position of last mouse down + */ + THIS[ this.hash ] = { + "mouseover": function( event ){ onMouseOver( _this, event ); }, + "mouseout": function( event ){ onMouseOut( _this, event ); }, + "mousedown": function( event ){ onMouseDown( _this, event ); }, + "mouseup": function( event ){ onMouseUp( _this, event ); }, + "click": function( event ){ onMouseClick( _this, event ); }, + "DOMMouseScroll": function( event ){ onMouseWheelSpin( _this, event ); }, + "mousewheel": function( event ){ onMouseWheelSpin( _this, event ); }, + "mouseupie": function( event ){ onMouseUpIE( _this, event ); }, + "mousemoveie": function( event ){ onMouseMoveIE( _this, event ); }, + "mouseupwindow": function( event ){ onMouseUpWindow( _this, event ); }, + "mousemove": function( event ){ onMouseMove( _this, event ); }, + tracking : false, + capturing : false, + buttonDown : false, + insideElement : false, + lastPoint : null, + lastMouseDownTime : null, + lastMouseDownPoint : null }; }; $.MouseTracker.prototype = { - /** - * @method + * Are we currently tracking events on this element. + * @deprecated Just use this.tracking + * @function + * @returns {Boolean} Are we currently tracking events on this element. */ isTracking: function () { - return this.tracking; + return THIS[ this.hash ].tracking; }, /** - * @method + * Enable or disable whether or not we are tracking events on this element. + * @function + * @param {Boolean} track True to start tracking, false to stop tracking. + * @returns {OpenSeadragon.MouseTracker} Chainable. */ setTracking: function ( track ) { if ( track ) { - this.startTracking(); + startTracking( this ); } else { - this.stopTracking(); + stopTracking( this ); } + //chain + return this; }, - - /** - * @method - */ - startTracking: function() { - if ( !this.tracking ) { - $.addEvent( this.element, "mouseover", this.delegates["mouseover"], false); - $.addEvent( this.element, "mouseout", this.delegates["mouseout"], false); - $.addEvent( this.element, "mousedown", this.delegates["mousedown"], false); - $.addEvent( this.element, "mouseup", this.delegates["mouseup"], false); - $.addEvent( this.element, "click", this.delegates["click"], false); - $.addEvent( this.element, "DOMMouseScroll", this.delegates["DOMMouseScroll"], false); - $.addEvent( this.element, "mousewheel", this.delegates["mousewheel"], false); // Firefox - - this.tracking = true; - ieTrackersActive[ this.hash ] = this; - } - }, - - /** - * @method - */ - stopTracking: function() { - if ( this.tracking ) { - $.removeEvent( this.element, "mouseover", this.delegates["mouseover"], false); - $.removeEvent( this.element, "mouseout", this.delegates["mouseout"], false); - $.removeEvent( this.element, "mousedown", this.delegates["mousedown"], false); - $.removeEvent( this.element, "mouseup", this.delegates["mouseup"], false); - $.removeEvent( this.element, "click", this.delegates["click"], false); - $.removeEvent( this.element, "DOMMouseScroll", this.delegates["DOMMouseScroll"], false); - $.removeEvent( this.element, "mousewheel", this.delegates["mousewheel"], false); - - this.releaseMouse(); - this.tracking = false; - delete ieTrackersActive[ this.hash ]; - } - }, - - /** - * @method - */ - captureMouse: function() { - if ( !this.capturing ) { - if ( $.Browser.vendor == $.BROWSERS.IE ) { - $.removeEvent( this.element, "mouseup", this.delegates["mouseup"], false ); - $.addEvent( this.element, "mouseup", this.delegates["mouseupie"], true ); - $.addEvent( this.element, "mousemove", this.delegates["mousemoveie"], true ); - } else { - $.addEvent( window, "mouseup", this.delegates["mouseupwindow"], true ); - $.addEvent( window, "mousemove", this.delegates["mousemove"], true ); - } - - this.capturing = true; - } - }, - /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Boolean} buttonDown + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @param {Boolean} buttonDownAny + * Was the button down anywhere in the screen during the event. */ - releaseMouse: function() { - if ( this.capturing ) { - if ( $.Browser.vendor == $.BROWSERS.IE ) { - $.removeEvent( this.element, "mousemove", this.delegates["mousemoveie"], true ); - $.removeEvent( this.element, "mouseup", this.delegates["mouseupie"], true ); - $.addEvent( this.element, "mouseup", this.delegates["mouseup"], false ); - } else { - $.removeEvent( window, "mousemove", this.delegates["mousemove"], true ); - $.removeEvent( window, "mouseup", this.delegates["mouseupwindow"], true ); - } - - this.capturing = false; - } - }, - + enterHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Boolean} buttonDown + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @param {Boolean} buttonDownAny + * Was the button down anywhere in the screen during the event. */ - triggerOthers: function( eventName, event ) { - var trackers = ieTrackersActive, - otherHash; - for ( otherHash in trackers ) { - if ( trackers.hasOwnProperty( otherHash ) && this.hash != otherHash ) { - trackers[ otherHash ][ eventName ]( event ); - } - } - }, + exitHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. */ - hasMouse: function() { - return this.insideElement; - }, - + pressHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Boolean} buttonDown + * True if the left mouse button is currently being pressed and was + * initiated inside the tracked element, otherwise false. + * @param {Boolean} insideElementRelease + * Was the mouse still inside the tracked element when the button + * was released. */ - onMouseOver: function( event ) { - var event = $.getEvent( event ); - - if ( $.Browser.vendor == $.BROWSERS.IE && - this.capturing && - !isChild( event.srcElement, this.element ) ) { - this.triggerOthers( "onMouseOver", event ); - } - - var to = event.target ? - event.target : - event.srcElement, - from = event.relatedTarget ? - event.relatedTarget : - event.fromElement; - - if ( !isChild( this.element, to ) || isChild( this.element, from ) ) { - return; - } - - this.insideElement = true; - - if ( typeof( this.enterHandler ) == "function") { - try { - this.enterHandler( - this, - getMouseRelative( event, this.element ), - this.buttonDownElement, - buttonDownAny - ); - } catch ( e ) { - $.console.error( - e.name + " while executing enter handler: " + e.message, - e - ); - } - } - }, + releaseHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Number} scroll + * The scroll delta for the event. + * @param {Boolean} shift + * Was the shift key being pressed during this event? */ - onMouseOut: function( event ) { - var event = $.getEvent( event ); - - if ( $.Browser.vendor == $.BROWSERS.IE && - this.capturing && - !isChild( event.srcElement, this.element ) ) { - this.triggerOthers( "onMouseOut", event ); - } - - var from = event.target ? - event.target : - event.srcElement, - to = event.relatedTarget ? - event.relatedTarget : - event.toElement; - - if ( !isChild( this.element, from ) || isChild( this.element, to ) ) { - return; - } - - this.insideElement = false; - - if ( typeof( this.exitHandler ) == "function" ) { - try { - this.exitHandler( - this, - getMouseRelative( event, this.element ), - this.buttonDownElement, - buttonDownAny - ); - } catch ( e ) { - $.console.error( - e.name + " while executing exit handler: " + e.message, - e - ); - } - } - }, + scrollHandler: function(){}, /** - * @method - * @inner + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {Boolean} quick + * True only if the clickDistThreshold and clickDeltaThreshold are + * both pased. Useful for ignoring events. + * @param {Boolean} shift + * Was the shift key being pressed during this event? */ - onMouseDown: function( event ) { - var event = $.getEvent( event ); - - if ( event.button == 2 ) { - return; - } - - this.buttonDownElement = true; - - this.lastPoint = getMouseAbsolute( event ); - this.lastMouseDownPoint = this.lastPoint; - this.lastMouseDownTime = new Date().getTime(); - - if ( typeof( this.pressHandler ) == "function" ) { - try { - this.pressHandler( - this, - getMouseRelative( event, this.element ) - ); - } catch (e) { - $.console.error( - e.name + " while executing press handler: " + e.message, - e - ); - } - } - - if ( this.pressHandler || this.dragHandler ) { - $.cancelEvent( event ); - } - - if ( !( $.Browser.vendor == $.BROWSERS.IE ) || !ieCapturingAny ) { - this.captureMouse(); - ieCapturingAny = true; - ieTrackersCapturing = [ this ]; // reset to empty & add us - } else if ( $.Browser.vendor == $.BROWSERS.IE ) { - ieTrackersCapturing.push( this ); // add us to the list - } - }, + clickHandler: function(){}, /** - * @method + * Implement or assign implmentation to these handlers during or after + * calling the constructor. + * @function + * @param {OpenSeadragon.MouseTracker} tracker + * A reference to the tracker instance. + * @param {OpenSeadragon.Point} position + * The poistion of the event on the screen. + * @param {OpenSeadragon.Point} delta + * The x,y components of the difference between start drag and + * end drag. Usefule for ignoring or weighting the events. + * @param {Boolean} shift + * Was the shift key being pressed during this event? */ - onMouseUp: function( event ) { - var event = $.getEvent( event ), - insideElementPress = this.buttonDownElement, - insideElementRelease = this.insideElement; - - if ( event.button == 2 ) { - return; - } - - this.buttonDownElement = false; - - if ( typeof( this.releaseHandler ) == "function" ) { - try { - this.releaseHandler( - this, - getMouseRelative( event, this.element ), - insideElementPress, - insideElementRelease - ); - } catch (e) { - $.console.error( - e.name + " while executing release handler: " + e.message, - e - ); - } - } - - if ( insideElementPress && insideElementRelease ) { - this.handleMouseClick( event ); - } - }, - - /** - * @method - * Only triggered once by the deepest element that initially received - * the mouse down event. We want to make sure THIS event doesn't bubble. - * Instead, we want to trigger the elements that initially received the - * mouse down event (including this one) only if the mouse is no longer - * inside them. Then, we want to release capture, and emulate a regular - * mouseup on the event that this event was meant for. - */ - onMouseUpIE: function( event ) { - var event = $.getEvent( event ), - tracker, - i; - - if ( event.button == 2 ) { - return; - } - - for ( i = 0; i < ieTrackersCapturing.length; i++ ) { - tracker = ieTrackersCapturing[ i ]; - if ( !tracker.hasMouse() ) { - tracker.onMouseUp( event ); - } - } - - this.releaseMouse(); - ieCapturingAny = false; - event.srcElement.fireEvent( - "on" + event.type, - document.createEventObject( event ) - ); - - $.stopEvent( event ); - }, - - /** - * @method - * Only triggered in W3C browsers by elements within which the mouse was - * initially pressed, since they are now listening to the window for - * mouseup during the capture phase. We shouldn't handle the mouseup - * here if the mouse is still inside this element, since the regular - * mouseup handler will still fire. - */ - onMouseUpWindow: function( event ) { - if ( !this.insideElement ) { - this.onMouseUp( event ); - } - - this.releaseMouse(); - }, - - /** - * @method - */ - onMouseClick: function( event ) { - - if ( this.clickHandler ) { - $.cancelEvent( event ); - } - }, - - /** - * @method - */ - onMouseWheelSpin: function( event ) { - var nDelta = 0; - - if ( !event ) { // For IE, access the global (window) event object - event = window.event; - } - - if ( event.wheelDelta ) { // IE and Opera - nDelta = event.wheelDelta; - if ( window.opera ) { // Opera has the values reversed - nDelta = -nDelta; - } - } else if (event.detail) { // Mozilla FireFox - nDelta = -event.detail; - } - - nDelta = nDelta > 0 ? 1 : -1; - - if ( typeof( this.scrollHandler ) == "function" ) { - try { - this.scrollHandler( - this, - getMouseRelative( event, this.element ), - nDelta, - event.shiftKey - ); - } catch (e) { - $.console.error( - e.name + " while executing scroll handler: " + e.message, - e - ); - } - - $.cancelEvent( event ); - } - }, - - /** - * @method - */ - handleMouseClick: function( event ) { - var event = $.getEvent( event ); - - if ( event.button == 2 ) { - return; - } - - var time = new Date().getTime() - this.lastMouseDownTime; - var point = getMouseAbsolute( event ); - var distance = this.lastMouseDownPoint.distanceTo( point ); - var quick = ( - time <= this.clickTimeThreshold - ) && ( - distance <= this.clickDistThreshold - ); - - if ( typeof( this.clickHandler ) == "function" ) { - try { - this.clickHandler( - this, - getMouseRelative( event, this.element ), - quick, - event.shiftKey - ); - } catch ( e ) { - $.console.error( - e.name + " while executing click handler: " + e.message, - e - ); - } - } - }, - - /** - * @method - */ - onMouseMove: function( event ) { - var event = $.getEvent( event ); - var point = getMouseAbsolute( event ); - var delta = point.minus( this.lastPoint ); - - this.lastPoint = point; - - if ( typeof( this.dragHandler ) == "function" ) { - try { - this.dragHandler( - this, - getMouseRelative( event, this.element ), - delta, - event.shiftKey - ); - } catch (e) { - $.console.error( - e.name + " while executing drag handler: " + e.message, - e - ); - } - - $.cancelEvent( event ); - } - }, - - /** - * Only triggered once by the deepest element that initially received - * the mouse down event. Since no other element has captured the mouse, - * we want to trigger the elements that initially received the mouse - * down event (including this one). - * @method - */ - onMouseMoveIE: function( event ) { - var i; - for ( i = 0; i < ieTrackersCapturing.length; i++ ) { - ieTrackersCapturing[ i ].onMouseMove( event ); - } - - $.stopEvent( event ); - } + dragHandler: function(){} }; /** - * @private - * @inner - */ + * Starts tracking mouse events on this element. + * @private + * @inner + */ + function startTracking( tracker ) { + var events = [ + "mouseover", "mouseout", "mousedown", "mouseup", "click", + "DOMMouseScroll", "mousewheel" + ], + delegate = THIS[ tracker.hash ], + event, + i; + + if ( !delegate.tracking ) { + for( i = 0; i < events.length; i++ ){ + event = events[ i ]; + $.addEvent( + tracker.element, + event, + delegate[ event ], + false + ); + } + delegate.tracking = true; + ACTIVE[ tracker.hash ] = tracker; + } + }; + + /** + * Stops tracking mouse events on this element. + * @private + * @inner + */ + function stopTracking( tracker ) { + var events = [ + "mouseover", "mouseout", "mousedown", "mouseup", "click", + "DOMMouseScroll", "mousewheel" + ], + delegate = THIS[ tracker.hash ], + event, + i; + + if ( delegate.tracking ) { + for( i = 0; i < events.length; i++ ){ + event = events[ i ]; + $.removeEvent( + tracker.element, + event, + delegate[ event ], + false + ); + } + + releaseMouse( tracker ); + delegate.tracking = false; + delete ACTIVE[ tracker.hash ]; + } + }; + + /** + * @private + * @inner + */ + function hasMouse( tracker ) { + return THIS[ tracker.hash ].insideElement; + }; + + /** + * Begin capturing mouse events on this element. + * @private + * @inner + */ + function captureMouse( tracker ) { + var delegate = THIS[ tracker.hash ]; + if ( !delegate.capturing ) { + + if ( $.Browser.vendor == $.BROWSERS.IE ) { + $.removeEvent( + tracker.element, + "mouseup", + delegate[ "mouseup" ], + false + ); + $.addEvent( + tracker.element, + "mouseup", + delegate[ "mouseupie" ], + true + ); + $.addEvent( + tracker.element, + "mousemove", + delegate[ "mousemoveie" ], + true + ); + } else { + $.addEvent( + window, + "mouseup", + delegate[ "mouseupwindow" ], + true + ); + $.addEvent( + window, + "mousemove", + delegate[ "mousemove" ], + true + ); + } + delegate.capturing = true; + } + }; + + + /** + * Stop capturing mouse events on this element. + * @private + * @inner + */ + function releaseMouse( tracker ) { + var delegate = THIS[ tracker.hash ]; + if ( delegate.capturing ) { + + if ( $.Browser.vendor == $.BROWSERS.IE ) { + $.removeEvent( + tracker.element, + "mousemove", + delegate[ "mousemoveie" ], + true + ); + $.removeEvent( + tracker.element, + "mouseup", + delegate[ "mouseupie" ], + true + ); + $.addEvent( + tracker.element, + "mouseup", + delegate[ "mouseup" ], + false + ); + } else { + $.removeEvent( + window, + "mousemove", + delegate[ "mousemove" ], + true + ); + $.removeEvent( + window, + "mouseup", + delegate[ "mouseupwindow" ], + true + ); + } + delegate.capturing = false; + } + }; + + /** + * @private + * @inner + */ + function triggerOthers( tracker, handler, event ) { + var otherHash; + for ( otherHash in ACTIVE ) { + if ( trackers.hasOwnProperty( otherHash ) && tracker.hash != otherHash ) { + handler( ACTIVE[ otherHash ], event ); + } + } + }; + + /** + * @private + * @inner + */ + function onMouseOver( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ]; + + if ( $.Browser.vendor == $.BROWSERS.IE && + delegate.capturing && + !isChild( event.srcElement, tracker.element ) ) { + + triggerOthers( tracker, onMouseOver, event ); + + } + + var to = event.target ? + event.target : + event.srcElement, + from = event.relatedTarget ? + event.relatedTarget : + event.fromElement; + + if ( !isChild( tracker.element, to ) || + isChild( tracker.element, from ) ) { + return; + } + + delegate.insideElement = true; + + if ( tracker.enterHandler ) { + try { + tracker.enterHandler( + tracker, + getMouseRelative( event, tracker.element ), + delegate.buttonDown, + IS_BUTTON_DOWN + ); + } catch ( e ) { + $.console.error( + "%s while executing enter handler: %s", + e.name, + e.message, + e + ); + } + } + }; + + /** + * @private + * @inner + */ + function onMouseOut( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ]; + + if ( $.Browser.vendor == $.BROWSERS.IE && + delegate.capturing && + !isChild( event.srcElement, tracker.element ) ) { + + triggerOthers( tracker, onMouseOut, event ); + + } + + var from = event.target ? + event.target : + event.srcElement, + to = event.relatedTarget ? + event.relatedTarget : + event.toElement; + + if ( !isChild( tracker.element, from ) || + isChild( tracker.element, to ) ) { + return; + } + + delegate.insideElement = false; + + if ( tracker.exitHandler ) { + try { + tracker.exitHandler( + tracker, + getMouseRelative( event, tracker.element ), + delegate.buttonDown, + IS_BUTTON_DOWN + ); + } catch ( e ) { + $.console.error( + "%s while executing exit handler: %s", + e.name, + e.message, + e + ); + } + } + }; + + /** + * @private + * @inner + */ + function onMouseDown( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ]; + + if ( event.button == 2 ) { + return; + } + + delegate.buttonDown = true; + + delegate.lastPoint = getMouseAbsolute( event ); + delegate.lastMouseDownPoint = delegate.lastPoint; + delegate.lastMouseDownTime = +new Date(); + + if ( tracker.pressHandler ) { + try { + tracker.pressHandler( + tracker, + getMouseRelative( event, tracker.element ) + ); + } catch (e) { + $.console.error( + "%s while executing press handler: %s", + e.name, + e.message, + e + ); + } + } + + if ( tracker.pressHandler || tracker.dragHandler ) { + $.cancelEvent( event ); + } + + if ( !( $.Browser.vendor == $.BROWSERS.IE ) || !IS_CAPTURING ) { + captureMouse( tracker ); + IS_CAPTURING = true; + // reset to empty & add us + CAPTURING = [ tracker ]; + } else if ( $.Browser.vendor == $.BROWSERS.IE ) { + // add us to the list + CAPTURING.push( tracker ); + } + }; + + /** + * @private + * @inner + */ + function onMouseUp( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ], + //were we inside the tracked element when we were pressed + insideElementPress = delegate.buttonDown, + //are we still inside the tracked element when we released + insideElementRelease = delegate.insideElement; + + if ( event.button == 2 ) { + return; + } + + delegate.buttonDown = false; + + if ( tracker.releaseHandler ) { + try { + tracker.releaseHandler( + tracker, + getMouseRelative( event, tracker.element ), + insideElementPress, + insideElementRelease + ); + } catch (e) { + $.console.error( + "%s while executing release handler: %s", + e.name, + e.message, + e + ); + } + } + + if ( insideElementPress && insideElementRelease ) { + handleMouseClick( tracker, event ); + } + }; + + /** + * Only triggered once by the deepest element that initially received + * the mouse down event. We want to make sure THIS event doesn't bubble. + * Instead, we want to trigger the elements that initially received the + * mouse down event (including this one) only if the mouse is no longer + * inside them. Then, we want to release capture, and emulate a regular + * mouseup on the event that this event was meant for. + * @private + * @inner + */ + function onMouseUpIE( tracker, event ) { + var event = $.getEvent( event ), + othertracker, + i; + + if ( event.button == 2 ) { + return; + } + + for ( i = 0; i < CAPTURING.length; i++ ) { + othertracker = CAPTURING[ i ]; + if ( !hasMouse( othertracker ) ) { + onMouseUp( othertracker, event ); + } + } + + releaseMouse( tracker ); + IS_CAPTURING = false; + event.srcElement.fireEvent( + "on" + event.type, + document.createEventObject( event ) + ); + + $.stopEvent( event ); + }; + + /** + * Only triggered in W3C browsers by elements within which the mouse was + * initially pressed, since they are now listening to the window for + * mouseup during the capture phase. We shouldn't handle the mouseup + * here if the mouse is still inside this element, since the regular + * mouseup handler will still fire. + * @private + * @inner + */ + function onMouseUpWindow( tracker, event ) { + if ( ! THIS[ tracker.hash ].insideElement ) { + onMouseUp( tracker, event ); + } + releaseMouse( tracker ); + }; + + /** + * @private + * @inner + */ + function onMouseClick( tracker, event ) { + if ( tracker.clickHandler ) { + $.cancelEvent( event ); + } + }; + + /** + * @private + * @inner + */ + function onMouseWheelSpin( tracker, event ) { + var nDelta = 0; + + if ( !event ) { // For IE, access the global (window) event object + event = window.event; + } + + if ( event.wheelDelta ) { // IE and Opera + nDelta = event.wheelDelta; + if ( window.opera ) { // Opera has the values reversed + nDelta = -nDelta; + } + } else if (event.detail) { // Mozilla FireFox + nDelta = -event.detail; + } + + nDelta = nDelta > 0 ? 1 : -1; + + if ( tracker.scrollHandler ) { + try { + tracker.scrollHandler( + tracker, + getMouseRelative( event, tracker.element ), + nDelta, + event.shiftKey + ); + } catch (e) { + $.console.error( + "%s while executing scroll handler: %s", + e.name, + e.message, + e + ); + } + + $.cancelEvent( event ); + } + }; + + /** + * @private + * @inner + */ + function handleMouseClick( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ]; + + if ( event.button == 2 ) { + return; + } + + var time = +new Date() - delegate.lastMouseDownTime, + point = getMouseAbsolute( event ), + distance = delegate.lastMouseDownPoint.distanceTo( point ), + quick = time <= tracker.clickTimeThreshold && + distance <= tracker.clickDistThreshold; + + if ( tracker.clickHandler ) { + try { + tracker.clickHandler( + tracker, + getMouseRelative( event, tracker.element ), + quick, + event.shiftKey + ); + } catch ( e ) { + $.console.error( + "%s while executing click handler: %s", + e.name, + e.message, + e + ); + } + } + }; + + /** + * @private + * @inner + */ + function onMouseMove( tracker, event ) { + var event = $.getEvent( event ), + delegate = THIS[ tracker.hash ], + point = getMouseAbsolute( event ), + delta = point.minus( delegate.lastPoint ); + + delegate.lastPoint = point; + + if ( tracker.dragHandler ) { + try { + tracker.dragHandler( + tracker, + getMouseRelative( event, tracker.element ), + delta, + event.shiftKey + ); + } catch (e) { + $.console.error( + "%s while executing drag handler: %s", + e.name, + e.message, + e + ); + } + + $.cancelEvent( event ); + } + }; + + /** + * Only triggered once by the deepest element that initially received + * the mouse down event. Since no other element has captured the mouse, + * we want to trigger the elements that initially received the mouse + * down event (including this one). The the param tracker isn't used + * but for consistency with the other event handlers we include it. + * @private + * @inner + */ + function onMouseMoveIE( tracker, event ) { + var i; + for ( i = 0; i < CAPTURING.length; i++ ) { + onMouseMove( CAPTURING[ i ], event ); + } + + $.stopEvent( event ); + }; + + /** + * @private + * @inner + */ function getMouseAbsolute( event ) { return $.getMousePosition( event ); }; @@ -569,7 +859,7 @@ * @inner */ function onGlobalMouseDown() { - buttonDownAny = true; + IS_BUTTON_DOWN = true; }; /** @@ -577,7 +867,7 @@ * @inner */ function onGlobalMouseUp() { - buttonDownAny = false; + IS_BUTTON_DOWN = false; }; diff --git a/src/viewer.js b/src/viewer.js index a893e1f7..e4ba27af 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -141,26 +141,24 @@ $.Viewer = function( options ) { this._forceRedraw = false; this._mouseInside = false; - this.innerTracker = new $.MouseTracker( - this.canvas, - this.config.clickTimeThreshold, - this.config.clickDistThreshold - ); - this.innerTracker.clickHandler = $.delegate( this, onCanvasClick ); - this.innerTracker.dragHandler = $.delegate( this, onCanvasDrag ); - this.innerTracker.releaseHandler = $.delegate( this, onCanvasRelease ); - this.innerTracker.scrollHandler = $.delegate( this, onCanvasScroll ); - this.innerTracker.setTracking( true ); // default state + this.innerTracker = new $.MouseTracker({ + element: this.canvas, + clickTimeThreshold: this.config.clickTimeThreshold, + clickDistThreshold: this.config.clickDistThreshold, + clickHandler: $.delegate( this, onCanvasClick ), + dragHandler: $.delegate( this, onCanvasDrag ), + releaseHandler: $.delegate( this, onCanvasRelease ), + scrollHandler: $.delegate( this, onCanvasScroll ) + }).setTracking( true ); // default state - this.outerTracker = new $.MouseTracker( - this.container, - this.config.clickTimeThreshold, - this.config.clickDistThreshold - ); - this.outerTracker.enterHandler = $.delegate( this, onContainerEnter ); - this.outerTracker.exitHandler = $.delegate( this, onContainerExit ); - this.outerTracker.releaseHandler = $.delegate( this, onContainerRelease ); - this.outerTracker.setTracking( true ); // always tracking + this.outerTracker = new $.MouseTracker({ + element: this.container, + clickTimeThreshold: this.config.clickTimeThreshold, + clickDistThreshold: this.config.clickDistThreshold, + enterHandler: $.delegate( this, onContainerEnter ), + exitHandler: $.delegate( this, onContainerExit ), + releaseHandler: $.delegate( this, onContainerRelease ) + }).setTracking( true ); // always tracking (function( canvas ){ canvas.width = "100%";