/* Copyright (c) 2007 Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net)
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*
* ALPHA
*/
 
(function($){
 
$.ScrollWindow = function(element, settings, callbacks) {
var _self = this;
 
// Get setting for this window
this.settings = $.extend({
relativeLinks : true,
theme : '',
id : '',
monitor : true,
autoEnable : true,
scrollIncrement : 12, // pixels
mouseWheel : true,
sliderMinHeight : 20 // pixels
}, settings || {});
 
// Get callbacks for this window
this.callbacks = $.extend({
scroll : null,
enable : null,
disable : null,
refresh : null,
monitor : null,
drag : null,
mousewheel : null
}, callbacks || {});
 
this.el = element;
this.$el = $(element);
 
// Add necessary HTML and store
this.$window = this.$el.wrap('<div id="' + this.settings.id + '" class="swWindow ' + this.settings.theme + '">').parent();
this.$content = this.$el.attr('id', '').addClass('swContent');
this.$track = $('<div class="swTrack">' ).appendTo( this.$window );
this.$arrowUp = $('<div class="swArrowUp">' ).appendTo( this.$window );
this.$arrowDown = $('<div class="swArrowDown">' ).appendTo( this.$window );
this.$slider = $('<div class="swSlider">' ).appendTo( this.$track );
this.$sliderTop = $('<div class="swSliderTop">' ).appendTo( this.$slider );
this.$sliderMiddle = $('<div class="swSliderMiddle">').appendTo( this.$slider );
this.$sliderBottom = $('<div class="swSliderBottom">').appendTo( this.$slider );
 
$.extend(this, {
version : 0.1,
 
monitorTimer : null,
currentHeight : null,
arrowTimeout : null,
arrowTimer : null,
trackTimeout : null,
trackTimer : null,
direction : null,
currentArrowInc : null,
lastMouseY : null,
trackMousePos : null,
dragMouseOffset : null,
contentPixels : null,
sliderPixels : null,
 
// Event Methods
addEvents : function() {
$('.swArrowUp, .swArrowDown', _self.$window)
.bind( 'mouseover', _self.onArrowMouseOver )
.bind( 'mouseout' , _self.onArrowMouseOut )
.bind( 'mousedown', _self.onArrowMouseDown )
.bind( 'mouseup' , _self.onArrowMouseUp );
 
_self.$slider
.bind( 'mouseover', _self.onSliderMouseOver )
.bind( 'mouseout' , _self.onSliderMouseOut )
.bind( 'mousedown', _self.onSliderMouseDown )
.bind( 'mouseup' , _self.onSliderMouseUp );
 
_self.$track
.bind( 'mouseout' , _self.onTrackMouseUp )
.bind( 'mousedown', _self.onTrackMouseDown )
.bind( 'mouseup' , _self.onTrackMouseUp );
 
if (_self.settings.mouseWheel && jQuery.fn.mousewheel)
_self.$window
.mousewheel(_self.onMouseWheel, false);
},
removeEvents : function() {
$('.swArrowUp, .swArrowDown', _self.$window)
.unbind( 'mouseover', _self.onArrowMouseOver )
.unbind( 'mouseout' , _self.onArrowMouseOut )
.unbind( 'mousedown', _self.onArrowMouseDown )
.unbind( 'mouseup' , _self.onArrowMouseUp );
 
_self.$slider
.unbind( 'mouseover', _self.onSliderMouseOver )
.unbind( 'mouseout' , _self.onSliderMouseOut )
.unbind( 'mousedown', _self.onSliderMouseDown )
.unbind( 'mouseup' , _self.onSliderMouseUp );
 
_self.$track
.unbind( 'mouseout' , _self.onTrackMouseUp )
.unbind( 'mousedown', _self.onTrackMouseDown )
.unbind( 'mouseup' , _self.onTrackMouseUp );
 
if (_self.settings.mouseWheel && jQuery.fn.mousewheel)
_self.$window
.unmousewheel(_self.onMouseWheel);
},
 
ignore : function() {
return false;
},
 
// Arrow Events
onArrowMouseOver : function() {
if (_self.dragMouseOffset) return;
 
var $el = $(this);
if ( $el.is('.swArrowUp') ) $el.addClass('swArrowUp-over');
else if ( $el.is('.swArrowDown') ) $el.addClass('swArrowDown-over');
},
onArrowMouseOut : function() {
var $el = $(this);
if ( $el.is('.swArrowUp') ) $el.removeClass('swArrowUp-over');
else if ( $el.is('.swArrowDown') ) $el.removeClass('swArrowDown-over');
 
_self.onArrowMouseUp.apply(this);
},
onArrowMouseDown : function() {
if (_self.settings.monitor) _self.unmonitor();
 
var $el = $(this);
$('body')
.bind('mouseup' , _self.onArrowMouseUp)
.bind('mouseout', _self.onArrowMouseUp);
 
if ( $el.is('.swArrowUp') ) {
_self.direction = -1;
$el.addClass('swArrowUp-active');
} else if ( $el.is('.swArrowDown') ) {
_self.direction = 1;
$el.addClass('swArrowDown-active');
}
 
_self.currentArrowInc = 0;
_self.whileArrowActive();
_self.arrowTimeout = window.setTimeout(function() {
_self.arrowTimer = window.setInterval(_self.whileArrowActive, 100);
}, 250);
 
return false;
},
onArrowMouseUp : function() {
if (_self.settings.monitor) _self.monitor();
 
if (_self.dragMouseOffset) return;
 
var $el = $(this);
 
$('body')
.unbind('mouseup' , _self.onArrowMouseUp)
.unbind('mouseout', _self.onArrowMouseUp);
 
if (_self.arrowTimeout) window.clearTimeout(_self.arrowTimeout);
if (_self.arrowTimer) window.clearInterval(_self.arrowTimer);
 
if ( $el.is('.swArrowUp') ) $el.removeClass('swArrowUp-active');
else if ( $el.is('.swArrowDown') ) $el.removeClass('swArrowDown-active');
},
whileArrowActive : function() {
var top = Math.abs( parseFloat( _self.$content.css('top') ) ) || 0,
increment = (_self.settings.scrollIncrement + (_self.settings.scrollIncrement * (_self.currentArrowInc/10)));
_self.jumpTo( top + (increment * _self.direction) );
_self.currentArrowInc++;
},
 
// Slider Events
onSliderMouseOver : function() {
_self.$slider.addClass('swSlider-over');
},
onSliderMouseOut : function() {
_self.$slider.removeClass('swSlider-over');
},
onSliderMouseDown : function(event) {
if (_self.settings.monitor) _self.unmonitor();
 
_self.$slider.addClass('swSlider-active');
 
var sliderTop = parseFloat( _self.$slider.css('top') );
 
_self.lastMouseY = event.pageY;
_self.dragMouseOffset = _self.lastMouseY - (_self.$track.offset().top + sliderTop);
 
$('body')
.bind('mouseup', _self.onSliderMouseUp)
.bind('mousemove', _self.onDrag);
 
if ($.browser.msie)
$('body')
.bind('dragstart', _self.ignore)
.bind('selectstart', _self.ignore);
 
return false;
},
onSliderMouseUp : function() {
if (_self.settings.monitor) _self.monitor();
_self.dragMouseOffset = null;
 
_self.$slider.removeClass('swSlider-active');
 
$('body')
.unbind('mouseup', _self.onSliderMouseUp)
.unbind('mousemove', _self.onDrag);
 
if ($.browser.msie)
$('body')
.unbind('dragstart', _self.ignore)
.unbind('selectstart', _self.ignore);
},
onDrag : function(event) {
if ( _self.callbacks.drag && _self.callbacks.drag() === false ) return;
 
var sliderTop = parseFloat( _self.$slider.css('top') ) || 0,
y = sliderTop + (event.pageY - _self.lastMouseY),
to = Math.min( Math.max(y, 0), (_self.$track.height() - _self.$slider.height()) );
 
_self.$slider .css('top', to);
_self.$content.css('top', to*_self.contentPixels*-1);
 
if ( y > to || y < to ) return false;
_self.lastMouseY = event.pageY;
return false;
},
 
// Track Events
onTrackMouseDown : function(event) {
if (_self.settings.monitor) _self.unmonitor();
 
_self.trackMousePos = event.pageY - _self.$track.offset().top;
 
$('body')
.bind('mouseup' , _self.onTrackMouseUp)
.bind('mouseout', _self.onTrackMouseUp);
 
_self.whileTrackActive();
_self.trackTimeout = setTimeout(function() {
_self.trackTimer = setInterval(_self.whileTrackActive, 100);
}, 250);
 
return false;
},
onTrackMouseUp : function() {
if (_self.settings.monitor) _self.monitor();
 
if (_self.dragMouseOffset) return;
 
if (_self.trackTimeout) clearTimeout(_self.trackTimeout);
if (_self.trackTimer) clearInterval(_self.trackTimer);
 
$('body')
.unbind('mouseup' , _self.onTrackMouseUp)
.unbind('mouseout', _self.onTrackMouseUp);
},
whileTrackActive : function() {
var sliderTop = parseFloat( _self.$slider.css('top') ) || 0, to;
 
if (sliderTop > _self.trackMousePos) to = sliderTop - _self.$slider.height();
else if (sliderTop < _self.trackMousePos) to = sliderTop + _self.$slider.height();
else _self.onTrackMouseUp();
 
to = Math.min( Math.max(to, 0), (_self.$track.height() - _self.$slider.height()) );
_self.$slider .css('top', to);
_self.$content.css('top', to*_self.contentPixels*-1);
},
 
// Mousewheel
onMouseWheel : function(event, delta) {
if ( _self.callbacks.mousewheel && _self.callbacks.mousewheel() === false ) return;
 
var top = Math.abs( parseFloat( _self.$content.css('top') ) ) || 0;
_self.jumpTo( top + ((_self.settings.scrollIncrement*2) * (delta * -1)) );
 
if ( top != Math.abs( parseFloat( _self.$content.css('top') ) ) ) {
event.preventDefault();
event.stopPropagation();
} // else if (  $(this).parents().filter('[@_mwHandler]') && $(this).parents().filter('[@_mwHandler]').length)
// event.preventDefault();
// }
},
 
/**
* Add events back to the scroll window and give it focus.
* Removes the 'disabled' class from the container.
*/
enable: function() {
if ( _self.callbacks.enable && _self.callbacks.enable() === false ) return;
_self.$window.removeClass('disabled');
 
_self.refresh();
if (_self.settings.monitor) _self.monitor();
},
 
/**
* Remove events from the scroll window and remove focus.
* Adds a class of 'disabled' to the container.
*/
disable: function() {
if ( _self.callbacks.disable && _self.callbacks.disable() === false ) return;
_self.$window.addClass('disabled');
_self.removeEvents();
if (_self.settings.monitor) _self.unmonitor();
},
 
/**
* Refreshes the scroll window.
*/
refresh: function() {
if ( this.callbacks.refresh && this.callbacks.refresh() === false ) return;
 
if ( this.$content.outerHeight() > this.$window.height() ) {
this.$window.removeClass('noScrollBar');
this.$slider.height( Math.max( ( this.$window.height() / this.$content.outerHeight() ) * this.$track.height(), this.settings.sliderMinHeight ) );
this.$sliderMiddle.css( 'top', ( (this.$slider.height() / 2) - (this.$sliderMiddle.height() / 2) ) );
 
this.contentPixels = (this.$content.outerHeight() - this.$window.outerHeight()) / (this.$track.height() - this.$slider.height());
this.sliderPixels = (this.$track.height() - this.$slider.height()) / (this.$content.outerHeight() - this.$window.height());
 
this.addEvents();
// if ( this.settings.relativeLinks ) this.setupRelativeLinks();
if ( false ) this.setupRelativeLinks();

} else {
this.$window.addClass('noScrollBar');
this.removeEvents();
}
},
 
/**
* Monitors the scroll window to watch for content changes and calls refresh when changed.
*/
monitor: function() {
if ( !_self.monitorTimer ) {
rate = (_self.settings.monitor == Number) ? _self.settings.monitor : 250;
_self.currentHeight = _self.$content.outerHeight();
_self.monitorTimer = setInterval( _self.monitor, rate );
_self.monitor();
} else {
if ( _self.callbacks.monitor && _self.callbacks.monitor() === false ) return;
if ( _self.currentHeight != _self.$content.outerHeight() ) {
_self.currentHeight = _self.$content.outerHeight();
_self.refresh();
}
}
},
 
/**
* Stops the monitoring of the scroll window.
*/
unmonitor: function() {
clearInterval( this.monitorTimer );
},
 
/**
* Prepares relative links to work within the scroll window.
*
* @param String|HTMLElement|jQuery context The parent of the relative a tags to be setup to work with the scroll window.
* Defaults to the content of the scroll window.
*/
setupRelativeLinks: function(context) {

		$('a[@href^="#"]', context || this.$content)
		.each(function() {
		if ( this._relativeSetup ) return;
		if ( this.hash.length > 1 ) {
		this._relativeSetup = true;
		$(this)
		.bind('click', function() {
		element = $(this.hash + ', a[@name=' + this.href.split("#")[1] + ']')[0];
		if ( element ) _self.jumpTo(element);
		return false;
		});
		}
		});
},
 
/**
* Jumps to an element or a position in the scroll window.
*
* @param Number|HTMLElement to Either a number that represents a Y-Coordinate or an HTMLElement to jump to.
*/
jumpTo: function(to) {
if ( this.$window.is('.noScrollBar') ) return;
if ( typeof to != 'number' ) to = ( $(to).offset().top - this.$window.offset().top );
 
to = Math.min( Math.max(to, 0), (this.$content.outerHeight() - this.$window.height()) );
 
this.$content.css('top', to*-1);
this.$slider .css('top', to*this.sliderPixels);
}
});
 
/**
* Adds HTML to the bottom of the scroll window. Acts just like the append method found in jQuery core.
*
* @name append
*/
/**
* Adds HTML to the top of the scroll window. Acts just like the prepend method found in jQuery core.
*
* @name prepend
*/
/**
* Replaces the HTML in the scroll window. Acts just like the html method found in jQuery core.
*
* @name html
*/
/**
* Clears the HTML in the scroll window. Acts just like the empty method found in jQuery core.
*
* @name empty
*/
$.each( ("append,prepend,html,empty").split(","), function(i,o){
_self[o] = function(html) {
_self.$content[o](html);
_self.refresh();
};
});
 
if (this.settings.autoEnable) this.enable();
 
/* $(':input', this.$content)
.bind('focus', this.removeEvents)
.bind('blur', this.addEvents);*/
 
return this;
};
 
$.fn.scrollWindow = function(settings, callbacks) {
return new $.ScrollWindow(this[0], settings, callbacks);
};
 
})(jQuery);