/* Copyright (c) 2006 Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com) * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)  * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. *  * See http://kelvinluck.com/assets/jquery/jScrollPane/ * $Id: jScrollPane.js 33 2008-12-10 22:55:28Z kelvin.luck $ *//** * Replace the vertical scroll bars on any matched elements with a fancy * styleable (via CSS) version. With JS disabled the elements will * gracefully degrade to the browsers own implementation of overflow:auto. * If the mousewheel plugin has been included on the page then the scrollable areas will also * respond to the mouse wheel. * * @example jQuery(".scroll-pane").jScrollPane(); * * @name jScrollPane * @type jQuery * @param Object	settings	hash with options, described below. *								scrollbarWidth	-	The width of the generated scrollbar in pixels *								scrollbarMargin	-	The amount of space to leave on the side of the scrollbar in pixels *								wheelSpeed		-	The speed the pane will scroll in response to the mouse wheel in pixels *								showArrows		-	Whether to display arrows for the user to scroll with *								arrowSize		-	The height of the arrow buttons if showArrows=true *								animateTo		-	Whether to animate when calling scrollTo and scrollBy *								dragMinHeight	-	The minimum height to allow the drag bar to be *								dragMaxHeight	-	The maximum height to allow the drag bar to be *								animateInterval	-	The interval in milliseconds to update an animating scrollPane (default 100) *								animateStep		-	The amount to divide the remaining scroll distance by when animating (default 3) *								maintainPosition-	Whether you want the contents of the scroll pane to maintain it's position when you re-initialise it - so it doesn't scroll as you add more content (default true) *								scrollbarOnLeft	-	Display the scrollbar on the left side?  (needs stylesheet changes, see examples.html) *								reinitialiseOnImageLoad - Whether the jScrollPane should automatically re-initialise itself when any contained images are loaded * @return jQuery * @cat Plugins/jScrollPane * @author Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com) */(function($) {$.jScrollPane = {	active : []};$.fn.jScrollPane = function(settings){	settings = $.extend({}, $.fn.jScrollPane.defaults, settings);	var rf = function() { return false; };		return this.each(		function()		{			var $this = $(this);			// Switch the element's overflow to hidden to ensure we get the size of the element without the scrollbars [http://plugins.jquery.com/node/1208]			$this.css('overflow', 'hidden');			var paneEle = this;						if ($(this).parent().is('.jScrollPaneContainer')) {				var currentScrollPosition = settings.maintainPosition ? $this.position().top : 0;								var $c = $(this).parent();				var paneWidth = $c.innerWidth();				var paneHeight = $c.outerHeight();				var trackHeight = paneHeight;				$('>.jScrollPaneTrack, >.jScrollArrowUp, >.jScrollArrowDown', $c).remove();				$this.css({'top':0});			} else {								var currentScrollPosition = 0;				this.originalPadding = $this.css('paddingTop') + ' ' + $this.css('paddingRight') + ' ' + $this.css('paddingBottom') + ' ' + $this.css('paddingLeft');				this.originalSidePaddingTotal = (parseInt($this.css('paddingLeft')) || 0) + (parseInt($this.css('paddingRight')) || 0);				var paneWidth = $this.innerWidth();				var paneHeight = $this.innerHeight();				var trackHeight = paneHeight;								$this.wrap(					$('<div></div>').attr(						{'className':'jScrollPaneContainer'}					).css(						{							'height':paneHeight+'px', 							'width':paneWidth+'px'						}					)				);				// deal with text size changes (if the jquery.em plugin is included)				// and re-initialise the scrollPane so the track maintains the				// correct size				$(document).bind(					'emchange', 					function(e, cur, prev)					{						$this.jScrollPane(settings);					}				);							}						if (settings.reinitialiseOnImageLoad) {				// code inspired by jquery.onImagesLoad: http://plugins.jquery.com/project/onImagesLoad				// except we re-initialise the scroll pane when each image loads so that the scroll pane is always up to size...				// TODO: Do I even need to store it in $.data? Is a local variable here the same since I don't pass the reinitialiseOnImageLoad when I re-initialise?				var $imagesToLoad = $.data(paneEle, 'jScrollPaneImagesToLoad') || $('img', $this);				var loadedImages = [];								if ($imagesToLoad.length) {					$imagesToLoad.each(function(i, val)	{						$(this).bind('load', function() {							if($.inArray(i, loadedImages) == -1){ //don't double count images								loadedImages.push(val); //keep a record of images we've seen								$imagesToLoad = $.grep($imagesToLoad, function(n, i) {									return n != val;								});								$.data(paneEle, 'jScrollPaneImagesToLoad', $imagesToLoad);								settings.reinitialiseOnImageLoad = false;								$this.jScrollPane(settings); // re-initialise							}						}).each(function(i, val) {							if(this.complete || this.complete===undefined) { 								//needed for potential cached images								this.src = this.src; 							} 						});					});				};			}			var p = this.originalSidePaddingTotal;						var cssToApply = {				'height':'auto',				'width':paneWidth - settings.scrollbarWidth - settings.scrollbarMargin - p + 'px'			}			if(settings.scrollbarOnLeft) {				cssToApply.paddingLeft = settings.scrollbarMargin + settings.scrollbarWidth + 'px';			} else {				cssToApply.paddingRight = settings.scrollbarMargin + 'px';			}			$this.css(cssToApply);			var contentHeight = $this.outerHeight();			var percentInView = paneHeight / contentHeight;			if (percentInView < .99) {				var $container = $this.parent();				$container.append(					$('<div></div>').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth+'px'}).append(						$('<div></div>').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth+'px'}).append(							$('<div></div>').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth+'px'}),							$('<div></div>').attr({'className':'jScrollPaneDragBottom'}).css({'width':settings.scrollbarWidth+'px'})						)					)				);								var $track = $('>.jScrollPaneTrack', $container);				var $drag = $('>.jScrollPaneTrack .jScrollPaneDrag', $container);								if (settings.showArrows) {										var currentArrowButton;					var currentArrowDirection;					var currentArrowInterval;					var currentArrowInc;					var whileArrowButtonDown = function()					{						if (currentArrowInc > 4 || currentArrowInc%4==0) {							positionDrag(dragPosition + currentArrowDirection * mouseWheelMultiplier);						}						currentArrowInc ++;					};					var onArrowMouseUp = function(event)					{						$('html').unbind('mouseup', onArrowMouseUp);						currentArrowButton.removeClass('jScrollActiveArrowButton');						clearInterval(currentArrowInterval);					};					var onArrowMouseDown = function() {						$('html').bind('mouseup', onArrowMouseUp);						currentArrowButton.addClass('jScrollActiveArrowButton');						currentArrowInc = 0;						whileArrowButtonDown();						currentArrowInterval = setInterval(whileArrowButtonDown, 100);					};					$container						.append(							$('<a></a>')								.attr({'href':'javascript:;', 'className':'jScrollArrowUp'})								.css({'width':settings.scrollbarWidth+'px'})								.html('Scroll up')								.bind('mousedown', function()								{									currentArrowButton = $(this);									currentArrowDirection = -1;									onArrowMouseDown();									this.blur();									return false;								})								.bind('click', rf),							$('<a></a>')								.attr({'href':'javascript:;', 'className':'jScrollArrowDown'})								.css({'width':settings.scrollbarWidth+'px'})								.html('Scroll down')								.bind('mousedown', function()								{									currentArrowButton = $(this);									currentArrowDirection = 1;									onArrowMouseDown();									this.blur();									return false;								})								.bind('click', rf)						);					var $upArrow = $('>.jScrollArrowUp', $container);					var $downArrow = $('>.jScrollArrowDown', $container);					if (settings.arrowSize) {						trackHeight = paneHeight - settings.arrowSize - settings.arrowSize;						$track							.css({'height': trackHeight+'px', top:settings.arrowSize+'px'})					} else {						var topArrowHeight = $upArrow.height();						settings.arrowSize = topArrowHeight;						trackHeight = paneHeight - topArrowHeight - $downArrow.height();						$track							.css({'height': trackHeight+'px', top:topArrowHeight+'px'})					}				}								var $pane = $(this).css({'position':'absolute', 'overflow':'visible'});								var currentOffset;				var maxY;				var mouseWheelMultiplier;				// store this in a seperate variable so we can keep track more accurately than just updating the css property..				var dragPosition = 0;				var dragMiddle = percentInView*paneHeight/2;								// pos function borrowed from tooltip plugin and adapted...				var getPos = function (event, c) {					var p = c == 'X' ? 'Left' : 'Top';					return event['page' + c] || (event['client' + c] + (document.documentElement['scroll' + p] || document.body['scroll' + p])) || 0;				};								var ignoreNativeDrag = function() {	return false; };								var initDrag = function()				{					ceaseAnimation();					currentOffset = $drag.offset(false);					currentOffset.top -= dragPosition;					maxY = trackHeight - $drag[0].offsetHeight;					mouseWheelMultiplier = 2 * settings.wheelSpeed * maxY / contentHeight;				};								var onStartDrag = function(event)				{					initDrag();					dragMiddle = getPos(event, 'Y') - dragPosition - currentOffset.top;					$('html').bind('mouseup', onStopDrag).bind('mousemove', updateScroll);					if ($.browser.msie) {						$('html').bind('dragstart', ignoreNativeDrag).bind('selectstart', ignoreNativeDrag);					}					return false;				};				var onStopDrag = function()				{					$('html').unbind('mouseup', onStopDrag).unbind('mousemove', updateScroll);					dragMiddle = percentInView*paneHeight/2;					if ($.browser.msie) {						$('html').unbind('dragstart', ignoreNativeDrag).unbind('selectstart', ignoreNativeDrag);					}				};				var positionDrag = function(destY)				{					destY = destY < 0 ? 0 : (destY > maxY ? maxY : destY);					dragPosition = destY;					$drag.css({'top':destY+'px'});					var p = destY / maxY;					$pane.css({'top':((paneHeight-contentHeight)*p) + 'px'});					$this.trigger('scroll');					if (settings.showArrows) {						$upArrow[destY == 0 ? 'addClass' : 'removeClass']('disabled');						$downArrow[destY == maxY ? 'addClass' : 'removeClass']('disabled');					}				};				var updateScroll = function(e)				{					positionDrag(getPos(e, 'Y') - currentOffset.top - dragMiddle);				};								var dragH = Math.max(Math.min(percentInView*(paneHeight-settings.arrowSize*2), settings.dragMaxHeight), settings.dragMinHeight);								$drag.css(					{'height':dragH+'px'}				).bind('mousedown', onStartDrag);								var trackScrollInterval;				var trackScrollInc;				var trackScrollMousePos;				var doTrackScroll = function()				{					if (trackScrollInc > 8 || trackScrollInc%4==0) {						positionDrag((dragPosition - ((dragPosition - trackScrollMousePos) / 2)));					}					trackScrollInc ++;				};				var onStopTrackClick = function()				{					clearInterval(trackScrollInterval);					$('html').unbind('mouseup', onStopTrackClick).unbind('mousemove', onTrackMouseMove);				};				var onTrackMouseMove = function(event)				{					trackScrollMousePos = getPos(event, 'Y') - currentOffset.top - dragMiddle;				};				var onTrackClick = function(event)				{					initDrag();					onTrackMouseMove(event);					trackScrollInc = 0;					$('html').bind('mouseup', onStopTrackClick).bind('mousemove', onTrackMouseMove);					trackScrollInterval = setInterval(doTrackScroll, 100);					doTrackScroll();				};								$track.bind('mousedown', onTrackClick);								$container.bind(					'mousewheel',					function (event, delta) {						initDrag();						ceaseAnimation();						var d = dragPosition;						positionDrag(dragPosition - delta * mouseWheelMultiplier);						var dragOccured = d != dragPosition;						return !dragOccured;					}				);				var _animateToPosition;				var _animateToInterval;				function animateToPosition()				{					var diff = (_animateToPosition - dragPosition) / settings.animateStep;					if (diff > 1 || diff < -1) {						positionDrag(dragPosition + diff);					} else {						positionDrag(_animateToPosition);						ceaseAnimation();					}				}				var ceaseAnimation = function()				{					if (_animateToInterval) {						clearInterval(_animateToInterval);						delete _animateToPosition;					}				};				var scrollTo = function(pos, preventAni)				{					if (typeof pos == "string") {						$e = $(pos, $this);						if (!$e.length) return;						pos = $e.offset().top - $this.offset().top;					}					$container.scrollTop(0);					ceaseAnimation();					var destDragPosition = -pos/(paneHeight-contentHeight) * maxY;					if (preventAni || !settings.animateTo) {						positionDrag(destDragPosition);					} else {						_animateToPosition = destDragPosition;						_animateToInterval = setInterval(animateToPosition, settings.animateInterval);					}				};				$this[0].scrollTo = scrollTo;								$this[0].scrollBy = function(delta)				{					var currentPos = -parseInt($pane.css('top')) || 0;					scrollTo(currentPos + delta);				};								initDrag();								scrollTo(-currentScrollPosition, true);							// Deal with it when the user tabs to a link or form element within this scrollpane				$('*', this).bind(					'focus',					function(event)					{						var $e = $(this);												// loop through parents adding the offset top of any elements that are relatively positioned between						// the focused element and the jScrollPaneContainer so we can get the true distance from the top						// of the focused element to the top of the scrollpane...						var eleTop = 0;												while ($e[0] != $this[0]) {							eleTop += $e.position().top;							$e = $e.offsetParent();						}												var viewportTop = -parseInt($pane.css('top')) || 0;						var maxVisibleEleTop = viewportTop + paneHeight;						var eleInView = eleTop > viewportTop && eleTop < maxVisibleEleTop;						if (!eleInView) {							var destPos = eleTop - settings.scrollbarMargin;							if (eleTop > viewportTop) { // element is below viewport - scroll so it is at bottom.								destPos += $(this).height() + 15 + settings.scrollbarMargin - paneHeight;							}							scrollTo(destPos);						}					}				)												if (location.hash) {					scrollTo(location.hash);				}								// use event delegation to listen for all clicks on links and hijack them if they are links to				// anchors within our content...				$(document).bind(					'click',					function(e)					{						$target = $(e.target);						if ($target.is('a')) {							var h = $target.attr('href');							if (h.substr(0, 1) == '#') {								scrollTo(h);							}						}					}				);								$.jScrollPane.active.push($this[0]);							} else {				$this.css(					{						'height':paneHeight+'px',						'width':paneWidth-this.originalSidePaddingTotal+'px',						'padding':this.originalPadding					}				);				// remove from active list?				$this.parent().unbind('mousewheel');			}					}	)};$.fn.jScrollPane.defaults = {	scrollbarWidth : 10,	scrollbarMargin : 5,	wheelSpeed : 18,	showArrows : false,	arrowSize : 0,	animateTo : false,	dragMinHeight : 1,	dragMaxHeight : 99999,	animateInterval : 100,	animateStep: 3,	maintainPosition: true,	scrollbarOnLeft: false,	reinitialiseOnImageLoad: false};// clean up the scrollTo expandos$(window)	.bind('unload', function() {		var els = $.jScrollPane.active; 		for (var i=0; i<els.length; i++) {			els[i].scrollTo = els[i].scrollBy = null;		}	});})(jQuery);
