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%";