text: 
  /* Tracks a drag's mouse movement, firing various handlers
  ----------------------------------------------------------------------------------------------------------------------*/
  
  var DragListener = fc.DragListener = Class.extend({
  
  	options: null,
  
  	isListening: false,
  	isDragging: false,
  
  	// coordinates of the initial mousedown
  	originX: null,
  	originY: null,
  
  	// handler attached to the document, bound to the DragListener's `this`
  	mousemoveProxy: null,
  	mouseupProxy: null,
  
  	// for IE8 bug-fighting behavior, for now
  	subjectEl: null, // the element being draged. optional
  	subjectHref: null,
  
  	scrollEl: null,
  	scrollBounds: null, // { top, bottom, left, right }
  	scrollTopVel: null, // pixels per second
  	scrollLeftVel: null, // pixels per second
  	scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
  	scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
  
  	scrollSensitivity: 30, // pixels from edge for scrolling to start
  	scrollSpeed: 200, // pixels per second, at maximum speed
  	scrollIntervalMs: 50, // millisecond wait between scroll increment
  
  
  	constructor: function(options) {
  		options = options || {};
  		this.options = options;
  		this.subjectEl = options.subjectEl;
  	},
  
  
  	// Call this when the user does a mousedown. Will probably lead to startListening
  	mousedown: function(ev) {
  		if (isPrimaryMouseButton(ev)) {
  
  			ev.preventDefault(); // prevents native selection in most browsers
  
  			this.startListening(ev);
  
  			// start the drag immediately if there is no minimum distance for a drag start
  			if (!this.options.distance) {
  				this.startDrag(ev);
  			}
  		}
  	},
  
  
  	// Call this to start tracking mouse movements
  	startListening: function(ev) {
  		var scrollParent;
  
  		if (!this.isListening) {
  
  			// grab scroll container and attach handler
  			if (ev && this.options.scroll) {
  				scrollParent = getScrollParent($(ev.target));
  				if (!scrollParent.is(window) && !scrollParent.is(document)) {
  					this.scrollEl = scrollParent;
  
  					// scope to `this`, and use `debounce` to make sure rapid calls don't happen
  					this.scrollHandlerProxy = debounce(proxy(this, 'scrollHandler'), 100);
  					this.scrollEl.on('scroll', this.scrollHandlerProxy);
  				}
  			}
  
  			$(document)
  				.on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'))
  				.on('mouseup', this.mouseupProxy = proxy(this, 'mouseup'))
  				.on('selectstart', this.preventDefault); // prevents native selection in IE<=8
  
  			if (ev) {
  				this.originX = ev.pageX;
  				this.originY = ev.pageY;
  			}
  			else {
  				// if no starting information was given, origin will be the topleft corner of the screen.
  				// if so, dx/dy in the future will be the absolute coordinates.
  				this.originX = 0;
  				this.originY = 0;
  			}
  
  			this.isListening = true;
  			this.listenStart(ev);
  		}
  	},
  
  
  	// Called when drag listening has started (but a real drag has not necessarily began)
  	listenStart: function(ev) {
  		this.trigger('listenStart', ev);
  	},
  
  
  	// Called when the user moves the mouse
  	mousemove: function(ev) {
  		var dx = ev.pageX - this.originX;
  		var dy = ev.pageY - this.originY;
  		var minDistance;
  		var distanceSq; // current distance from the origin, squared
  
  		if (!this.isDragging) { // if not already dragging...
  			// then start the drag if the minimum distance criteria is met
  			minDistance = this.options.distance || 1;
  			distanceSq = dx * dx + dy * dy;
  			if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
  				this.startDrag(ev);
  			}
  		}
  
  		if (this.isDragging) {
  			this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag
  		}
  	},
  
  
  	// Call this to initiate a legitimate drag.
  	// This function is called internally from this class, but can also be called explicitly from outside
  	startDrag: function(ev) {
  
  		if (!this.isListening) { // startDrag must have manually initiated
  			this.startListening();
  		}
  
  		if (!this.isDragging) {
  			this.isDragging = true;
  			this.dragStart(ev);
  		}
  	},
  
  
  	// Called when the actual drag has started (went beyond minDistance)
  	dragStart: function(ev) {
  		var subjectEl = this.subjectEl;
  
  		this.trigger('dragStart', ev);
  
  		// remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
  		if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
  			subjectEl.removeAttr('href');
  		}
  	},
  
  
  	// Called while the mouse is being moved and when we know a legitimate drag is taking place
  	drag: function(dx, dy, ev) {
  		this.trigger('drag', dx, dy, ev);
  		this.updateScroll(ev); // will possibly cause scrolling
  	},
  
  
  	// Called when the user does a mouseup
  	mouseup: function(ev) {
  		this.stopListening(ev);
  	},
  
  
  	// Called when the drag is over. Will not cause listening to stop however.
  	// A concluding 'cellOut' event will NOT be triggered.
  	stopDrag: function(ev) {
  		if (this.isDragging) {
  			this.stopScrolling();
  			this.dragStop(ev);
  			this.isDragging = false;
  		}
  	},
  
  
  	// Called when dragging has been stopped
  	dragStop: function(ev) {
  		var _this = this;
  
  		this.trigger('dragStop', ev);
  
  		// restore a mousedown'd <a>'s href (for IE8 bug)
  		setTimeout(function() { // must be outside of the click's execution
  			if (_this.subjectHref) {
  				_this.subjectEl.attr('href', _this.subjectHref);
  			}
  		}, 0);
  	},
  
  
  	// Call this to stop listening to the user's mouse events
  	stopListening: function(ev) {
  		this.stopDrag(ev); // if there's a current drag, kill it
  
  		if (this.isListening) {
  
  			// remove the scroll handler if there is a scrollEl
  			if (this.scrollEl) {
  				this.scrollEl.off('scroll', this.scrollHandlerProxy);
  				this.scrollHandlerProxy = null;
  			}
  
  			$(document)
  				.off('mousemove', this.mousemoveProxy)
  				.off('mouseup', this.mouseupProxy)
  				.off('selectstart', this.preventDefault);
  
  			this.mousemoveProxy = null;
  			this.mouseupProxy = null;
  
  			this.isListening = false;
  			this.listenStop(ev);
  		}
  	},
  
  
  	// Called when drag listening has stopped
  	listenStop: function(ev) {
  		this.trigger('listenStop', ev);
  	},
  
  
  	// Triggers a callback. Calls a function in the option hash of the same name.
  	// Arguments beyond the first `name` are forwarded on.
  	trigger: function(name) {
  		if (this.options[name]) {
  			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  		}
  	},
  
  
  	// Stops a given mouse event from doing it's native browser action. In our case, text selection.
  	preventDefault: function(ev) {
  		ev.preventDefault();
  	},
  
  
  	/* Scrolling
  	------------------------------------------------------------------------------------------------------------------*/
  
  
  	// Computes and stores the bounding rectangle of scrollEl
  	computeScrollBounds: function() {
  		var el = this.scrollEl;
  
  		this.scrollBounds = el ? getOuterRect(el) : null;
  			// TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
  	},
  
  
  	// Called when the dragging is in progress and scrolling should be updated
  	updateScroll: function(ev) {
  		var sensitivity = this.scrollSensitivity;
  		var bounds = this.scrollBounds;
  		var topCloseness, bottomCloseness;
  		var leftCloseness, rightCloseness;
  		var topVel = 0;
  		var leftVel = 0;
  
  		if (bounds) { // only scroll if scrollEl exists
  
  			// compute closeness to edges. valid range is from 0.0 - 1.0
  			topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
  			bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
  			leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
  			rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
  
  			// translate vertical closeness into velocity.
  			// mouse must be completely in bounds for velocity to happen.
  			if (topCloseness >= 0 && topCloseness <= 1) {
  				topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
  			}
  			else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
  				topVel = bottomCloseness * this.scrollSpeed;
  			}
  
  			// translate horizontal closeness into velocity
  			if (leftCloseness >= 0 && leftCloseness <= 1) {
  				leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
  			}
  			else if (rightCloseness >= 0 && rightCloseness <= 1) {
  				leftVel = rightCloseness * this.scrollSpeed;
  			}
  		}
  
  		this.setScrollVel(topVel, leftVel);
  	},
  
  
  	// Sets the speed-of-scrolling for the scrollEl
  	setScrollVel: function(topVel, leftVel) {
  
  		this.scrollTopVel = topVel;
  		this.scrollLeftVel = leftVel;
  
  		this.constrainScrollVel(); // massages into realistic values
  
  		// if there is non-zero velocity, and an animation loop hasn't already started, then START
  		if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
  			this.scrollIntervalId = setInterval(
  				proxy(this, 'scrollIntervalFunc'), // scope to `this`
  				this.scrollIntervalMs
  			);
  		}
  	},
  
  
  	// Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
  	constrainScrollVel: function() {
  		var el = this.scrollEl;
  
  		if (this.scrollTopVel < 0) { // scrolling up?
  			if (el.scrollTop() <= 0) { // already scrolled all the way up?
  				this.scrollTopVel = 0;
  			}
  		}
  		else if (this.scrollTopVel > 0) { // scrolling down?
  			if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
  				this.scrollTopVel = 0;
  			}
  		}
  
  		if (this.scrollLeftVel < 0) { // scrolling left?
  			if (el.scrollLeft() <= 0) { // already scrolled all the left?
  				this.scrollLeftVel = 0;
  			}
  		}
  		else if (this.scrollLeftVel > 0) { // scrolling right?
  			if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
  				this.scrollLeftVel = 0;
  			}
  		}
  	},
  
  
  	// This function gets called during every iteration of the scrolling animation loop
  	scrollIntervalFunc: function() {
  		var el = this.scrollEl;
  		var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
  
  		// change the value of scrollEl's scroll
  		if (this.scrollTopVel) {
  			el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
  		}
  		if (this.scrollLeftVel) {
  			el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
  		}
  
  		this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
  
  		// if scrolled all the way, which causes the vels to be zero, stop the animation loop
  		if (!this.scrollTopVel && !this.scrollLeftVel) {
  			this.stopScrolling();
  		}
  	},
  
  
  	// Kills any existing scrolling animation loop
  	stopScrolling: function() {
  		if (this.scrollIntervalId) {
  			clearInterval(this.scrollIntervalId);
  			this.scrollIntervalId = null;
  
  			// when all done with scrolling, recompute positions since they probably changed
  			this.scrollStop();
  		}
  	},
  
  
  	// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
  	scrollHandler: function() {
  		// recompute all coordinates, but *only* if this is *not* part of our scrolling animation
  		if (!this.scrollIntervalId) {
  			this.scrollStop();
  		}
  	},
  
  
  	// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  	scrollStop: function() {
  	}
  
  });
  