/*!
 * jScrollPane - v2.0.0beta9 - 2011-01-31
 * http://jscrollpane.kelvinluck.com/
 *
 * Copyright (c) 2010 Kelvin Luck
 * Dual licensed under the MIT and GPL licenses.
 */

// Script: jScrollPane - cross browser customisable scrollbars
//
// *Version: 2.0.0beta9, Last updated: 2011-01-31*
//
// Project Home - http://jscrollpane.kelvinluck.com/
// GitHub       - http://github.com/vitch/jScrollPane
// Source       - http://github.com/vitch/jScrollPane/raw/master/script/jquery.jscrollpane.js
// (Minified)   - http://github.com/vitch/jScrollPane/raw/master/script/jquery.jscrollpane.min.js
//
// About: License
//
// Copyright (c) 2010 Kelvin Luck
// Dual licensed under the MIT or GPL Version 2 licenses.
// http://jscrollpane.kelvinluck.com/MIT-LICENSE.txt
// http://jscrollpane.kelvinluck.com/GPL-LICENSE.txt
//
// About: Examples
//
// All examples and demos are available through the jScrollPane example site at:
// http://jscrollpane.kelvinluck.com/
//
// About: Support and Testing
//
// This plugin is tested on the browsers below and has been found to work reliably on them. If you run
// into a problem on one of the supported browsers then please visit the support section on the jScrollPane
// website (http://jscrollpane.kelvinluck.com/) for more information on getting support. You are also
// welcome to fork the project on GitHub if you can contribute a fix for a given issue. 
//
// jQuery Versions - tested in 1.4.2+ - reported to work in 1.3.x
// Browsers Tested - Firefox 3.6.8, Safari 5, Opera 10.6, Chrome 5.0, IE 6, 7, 8
//
// About: Release History
//
// 2.0.0beta9 - (2011-01-31) new API methods, bug fixes and correct keyboard support for FF/OSX
// 2.0.0beta8 - (2011-01-29) touchscreen support, improved keyboard support
// 2.0.0beta7 - (2011-01-23) scroll speed consistent (thanks Aivo Paas)
// 2.0.0beta6 - (2010-12-07) scrollToElement horizontal support
// 2.0.0beta5 - (2010-10-18) jQuery 1.4.3 support, various bug fixes
// 2.0.0beta4 - (2010-09-17) clickOnTrack support, bug fixes
// 2.0.0beta3 - (2010-08-27) Horizontal mousewheel, mwheelIntent, keyboard support, bug fixes
// 2.0.0beta2 - (2010-08-21) Bug fixes
// 2.0.0beta1 - (2010-08-17) Rewrite to follow modern best practices and enable horizontal scrolling, initially hidden
//							 elements and dynamically sized elements.
// 1.x - (2006-12-31 - 2010-07-31) Initial version, hosted at googlecode, deprecated

(function ($,window,undefined) {

	$.fn.jScrollPane = function (settings) {
		// JScrollPane "class" - public methods are available through $('selector').data('jsp')
		function JScrollPane(elem, s) {
			var settings, 
				jsp = this, 
				pane, 
				paneWidth, 
				paneHeight, 
				container, 
				contentWidth, 
				contentHeight,
				percentInViewH, 
				percentInViewV, 
				isScrollableV, 
				isScrollableH, 
				verticalDrag, 
				dragMaxY,
				verticalDragPosition, 
				horizontalDrag, 
				dragMaxX, 
				horizontalDragPosition,
				verticalBar, 
				verticalTrack, 
				scrollbarWidth, 
				verticalTrackHeight, 
				verticalDragHeight, 
				arrowUp, 
				arrowDown,
				horizontalBar, 
				horizontalTrack, 
				horizontalTrackWidth, 
				horizontalDragWidth, 
				arrowLeft, 
				arrowRight,
				reinitialiseInterval, 
				originalPadding, 
				originalPaddingTotalWidth, 
				previousContentWidth,
				wasAtTop = true, 
				wasAtLeft = true, 
				wasAtBottom = false, 
				wasAtRight = false,
				originalElement = elem.clone().empty(),																								
				mwEvent = $.fn.mwheelIntent ? 'mwheelIntent.jsp' : 'mousewheel.jsp';

			originalPadding				= 	elem.css('paddingTop') 		+ ' ' +																		// padding top 		de l'element traité
											elem.css('paddingRight') 	+ ' ' +																		// padding right 	de l'element traité
											elem.css('paddingBottom') 	+ ' ' +																		// padding bottom 	de l'element traité
											elem.css('paddingLeft');																				// padding left 	de l'element traité
			
			originalPaddingTotalWidth 	= 	(parseInt(elem.css('paddingLeft')	, 10) || 0) + (parseInt(elem.css('paddingRight')	, 10) || 0);	// largeur totale du padding left et right de l'element traité
			
			function initialise(s) {
				
				var clonedElem, 
					tempWrapper, 																													// firstChild, lastChild,
					isMaintainingPositon, 
					lastContentX, 
					lastContentY,
					hasContainingSpaceChanged, 
					originalScrollTop, 
					originalScrollLeft;

				settings = s;

				if (pane === undefined) {																											// si l'élément "$('<div class="jspPane"></div>')" n'a pas été créé
					
					originalScrollTop 	= elem.scrollTop();
					originalScrollLeft 	= elem.scrollLeft();
					
					elem.css({
						overflow: 'hidden',
						padding: 0
					});
					
					// TODO: Deal with where width/ height is 0 as it probably means the element is hidden and we should
					// come back to it later and check once it is unhidden...
					paneWidth 	= elem.innerWidth() + originalPaddingTotalWidth;																	// width (padding left et right inclus) de l'element traité
					paneHeight 	= elem.innerHeight(); 																								// le height de l'element traité
					
					elem.width(paneWidth);																											// attribution du width (padding left et right inclus) de l'element traité
					
					pane 		= $('<div class="jspPane"></div>')																					// génération de la div jspPane 
										.css('padding', originalPadding)																			// attribution du padding de l'élément traité
										.append(elem.children());																					// insertion du contenu de l'élément traité
					
					container 	= $('<div class="jspContainer"></div>')																				// génération de la div jspContainer 
										.css({ 'width': paneWidth + 'px', 'height': paneHeight + 'px' })											// atribution du width (padding left et right inclus) et du height de l'élément traité 
										.append(pane)																								// insertion du contenu de pane
										.appendTo(elem);																							// insertion de l'ensemble de container nouvellement créer dans l'élément traité
					
					/*
					// Move any margins from the first and last children up to the container so they can still
					// collapse with neighbouring elements as they would before jScrollPane 
					firstChild = pane.find(':first-child');
					lastChild = pane.find(':last-child');
					elem.css(
						{
							'margin-top': firstChild.css('margin-top'),
							'margin-bottom': lastChild.css('margin-bottom')
						}
					);
					firstChild.css('margin-top', 0);
					lastChild.css('margin-bottom', 0);
					*/
					
				} else {																																			// si l'élément "$('<div class="jspPane"></div>')" a été créé
					
					elem.css('width', '');																															// vide du width de l'élément traité

					hasContainingSpaceChanged = ( (	elem.innerWidth() + originalPaddingTotalWidth	) 	!= paneWidth ) || ( elem.outerHeight()	!= paneHeight );

					if ( hasContainingSpaceChanged ) {
						paneWidth 	= elem.innerWidth() + originalPaddingTotalWidth;
						paneHeight 	= elem.innerHeight();
						
						container.css({
							width	: paneWidth + 'px',
							height	: paneHeight + 'px'
						});
					} // END if ( hasContainingSpaceChanged )

					// If nothing changed since last check...
					if ( !hasContainingSpaceChanged && previousContentWidth == contentWidth && pane.outerHeight() == contentHeight ) {
						elem.width(paneWidth);
						return;
					} // END IF
					
					previousContentWidth = contentWidth;
					
					pane.css('width', '');
					elem.width(paneWidth);

					container.find('>.jspVerticalBar,>.jspHorizontalBar').remove().end();
				
				} // END IF (pane === undefined)
				
				
				
				// Unfortunately it isn't that easy to find out the width of the element as it will always report the
				// width as allowed by its container, regardless of overflow settings.
				// A cunning workaround is to clone the element, set its position to absolute and place it in a narrow
				// container. Now it will push outwards to its maxium real width...
				clonedElem		= pane.clone().css('position', 'absolute');
				
				tempWrapper		= $('<div style="width:1px; position: relative;" />').append(clonedElem);
				
				$('body').append(tempWrapper);
				
				contentWidth = Math.max( pane.outerWidth(), clonedElem.outerWidth() );
				
				tempWrapper.remove();
				
				contentHeight	= pane.outerHeight();
				percentInViewH	= contentWidth / paneWidth;
				percentInViewV	= contentHeight / paneHeight;
				isScrollableV	= percentInViewV > 1;

				isScrollableH = percentInViewH > 1;

				// console.log(paneWidth, paneHeight, contentWidth, contentHeight, percentInViewH, percentInViewV, isScrollableH, isScrollableV);
				if ( !(isScrollableH || isScrollableV) ) {																							// Est-ce que le contenu de l'élément traité est plus grand que la zone d'overflow
					
					elem.removeClass('jspScrollable');
					
					pane.css({
						top: 0,
						width: container.width() - originalPaddingTotalWidth
					});
					
					removeMousewheel();
					removeFocusHandler();
					removeKeyboardNav();
					removeClickOnTrack();
					unhijackInternalLinks();
					
				} else {
					
					elem.addClass('jspScrollable');

					isMaintainingPositon = settings.maintainPosition && (verticalDragPosition || horizontalDragPosition);
					
					if ( isMaintainingPositon ) {
						lastContentX = contentPositionX();
						lastContentY = contentPositionY();
					} // END if ( isMaintainingPositon )

					initialiseVerticalScroll();
					
					initialiseHorizontalScroll();
					
					resizeScrollbars();

					if ( isMaintainingPositon ) {
						scrollToX(lastContentX, false);
						scrollToY(lastContentY, false);
					} // END IF

					initFocusHandler();
					initMousewheel();
					initTouch();
					
					if (settings.enableKeyboardNavigation) {
						initKeyboardNav();
					} // END IF
					
					if (settings.clickOnTrack) {
						initClickOnTrack();
					} // END IF
					
					observeHash();
					
					if ( settings.hijackInternalLinks ) {
						hijackInternalLinks();
					} // END IF
				
				} // END if ( !(isScrollableH || isScrollableV) )

				if ( settings.autoReinitialise && !reinitialiseInterval ) {
					reinitialiseInterval = setInterval( function() {
															initialise(settings);
														}, settings.autoReinitialiseDelay );
					
				} else if (!settings.autoReinitialise && reinitialiseInterval) {
					clearInterval(reinitialiseInterval);
				} // END IF 

				originalScrollTop && elem.scrollTop(0) && scrollToY(originalScrollTop, false);
				originalScrollLeft && elem.scrollLeft(0) && scrollToX(originalScrollLeft, false);

				elem.trigger('jsp-initialised', [isScrollableH || isScrollableV]);
			
			} // END function initialise(s)

			function initialiseVerticalScroll() {
			
				if ( isScrollableV ) {
				
					// contenu du container
					container	.append(	
									$('<div class="jspVerticalBar" />')	.append(	
																			$('<div class="jspCap jspCapTop" />'),
																			$('<div class="jspTrack" />')	.append(	
																												$('<div class="jspDrag" />').append(	
																																				$('<div class="jspDragTop" />'),
																																				$('<div class="jspDragBottom" />')
																																			)
																											),
																			$('<div class="jspCap jspCapBottom" />')
																		)
								);

					verticalBar		= container		.find('>.jspVerticalBar');
					verticalTrack	= verticalBar	.find('>.jspTrack');
					verticalDrag	= verticalTrack	.find('>.jspDrag');

					// montrer les flèches de scroll
					if ( settings.showArrows ) {
					
						arrowUp		= $('<a class="jspArrow jspArrowUp" />')	.bind( 'mousedown.jsp', getArrowScroll(0, -1) )
																				.bind( 'click.jsp', nil );
						
						arrowDown	= $('<a class="jspArrow jspArrowDown" />')	.bind( 'mousedown.jsp', getArrowScroll(0, 1)  )
																				.bind( 'click.jsp', nil );
						
						if ( settings.arrowScrollOnHover ) {
							arrowUp		.bind( 'mouseover.jsp', getArrowScroll(0, -1, arrowUp)  );
							arrowDown	.bind( 'mouseover.jsp', getArrowScroll(0, 1, arrowDown) );
						}

						appendArrows( verticalTrack, settings.verticalArrowPositions, arrowUp, arrowDown );
					
					} // END if ( settings.showArrows )

					verticalTrackHeight = paneHeight;
					
					container	.find('>.jspVerticalBar>.jspCap:visible,>.jspVerticalBar>.jspArrow')
								.each( function() {
									verticalTrackHeight -= $(this).outerHeight();
								});
					
					verticalDrag	.hover( 
										function() {
											verticalDrag.addClass('jspHover');
										},
										function() {
											verticalDrag.removeClass('jspHover');
										}
									)
									.bind( 'mousedown.jsp', function(e) {
																// Stop IE from allowing text selection
																$('html').bind('dragstart.jsp selectstart.jsp', nil);
																verticalDrag.addClass('jspActive');
																var startY = e.pageY - verticalDrag.position().top;
																$('html')	.bind( 'mousemove.jsp', function(e) {
																										positionDragY(e.pageY - startY, false);
																									}
																			)
																			.bind( 'mouseup.jsp mouseleave.jsp', cancelDrag);
																return false;
															}
									);
					sizeVerticalScrollbar();
				
				} // END if ( isScrollableV )
			
			} // END function initialiseVerticalScroll()

			function sizeVerticalScrollbar()
			{
				verticalTrack.height(verticalTrackHeight + 'px');
				verticalDragPosition = 0;
				scrollbarWidth = settings.verticalGutter + verticalTrack.outerWidth();

				// Make the pane thinner to allow for the vertical scrollbar
				pane.width(paneWidth - scrollbarWidth - originalPaddingTotalWidth);

				// Add margin to the left of the pane if scrollbars are on that side (to position
				// the scrollbar on the left or right set it's left or right property in CSS)
				if (verticalBar.position().left === 0) {
					pane.css('margin-left', scrollbarWidth + 'px');
				}
			}

			function initialiseHorizontalScroll() {
				if ( isScrollableH ) {

					container	.append(	
									$('<div class="jspHorizontalBar" />')	.append(	
																				$('<div class="jspCap jspCapLeft" />'),
																				$('<div class="jspTrack" />')	.append(	
																													$('<div class="jspDrag" />').append(	
																																					$('<div class="jspDragLeft" />'),
																																					$('<div class="jspDragRight" />')
																																				)
																												),
																				$('<div class="jspCap jspCapRight" />')
																			)
								);

					horizontalBar 	= container			.find('>.jspHorizontalBar');
					horizontalTrack = horizontalBar		.find('>.jspTrack');
					horizontalDrag 	= horizontalTrack	.find('>.jspDrag');

					if ( settings.showArrows ) {
					
						arrowLeft	= $('<a class="jspArrow jspArrowLeft" />')	.bind( 'mousedown.jsp'	, getArrowScroll(-1, 0) )
																				.bind( 'click.jsp'		, nil );
						arrowRight	= $('<a class="jspArrow jspArrowRight" />')	.bind( 'mousedown.jsp'	, getArrowScroll(1, 0)  )
																				.bind( 'click.jsp'		, nil 					);
						if ( settings.arrowScrollOnHover ) {
						
							arrowLeft	.bind( 'mouseover.jsp', getArrowScroll(-1, 0, arrowLeft) );
							arrowRight	.bind( 'mouseover.jsp', getArrowScroll(1, 0, arrowRight) );
						
						} // END if ( settings.arrowScrollOnHover )
						
						appendArrows( horizontalTrack, settings.horizontalArrowPositions, arrowLeft, arrowRight );
					
					} // END if ( settings.showArrows )

					horizontalDrag	.hover(
										function() {
											horizontalDrag.addClass('jspHover');
										},
										function() {
											horizontalDrag.removeClass('jspHover');
										}
									)
									.bind( 'mousedown.jsp', function(e)	{
																// Stop IE from allowing text selection
																$('html')	.bind( 'dragstart.jsp selectstart.jsp', nil );

																horizontalDrag.addClass('jspActive');

																var startX = e.pageX - horizontalDrag.position().left;

																$('html')	.bind( 'mousemove.jsp', function(e) {
																										positionDragX(e.pageX - startX, false);
																									}
																			)
																			.bind( 'mouseup.jsp mouseleave.jsp', cancelDrag );
																return false;
															}
									);
					
					horizontalTrackWidth = container.innerWidth();
					
					sizeHorizontalScrollbar();
				
				} // END if ( isScrollableH )
			
			} // END function initialiseHorizontalScroll()

			function sizeHorizontalScrollbar()
			{
				container.find('>.jspHorizontalBar>.jspCap:visible,>.jspHorizontalBar>.jspArrow').each(
					function()
					{
						horizontalTrackWidth -= $(this).outerWidth();
					}
				);

				horizontalTrack.width(horizontalTrackWidth + 'px');
				horizontalDragPosition = 0;
			}

			function resizeScrollbars() {
			
				if ( isScrollableH && isScrollableV ) {
				
					var horizontalTrackHeight	= horizontalTrack	.outerHeight(),
						verticalTrackWidth 		= verticalTrack		.outerWidth();
					
					verticalTrackHeight -= horizontalTrackHeight;
					
					$(horizontalBar)	.find( '>.jspCap:visible,>.jspArrow')
										.each( function() {
													horizontalTrackWidth += $(this).outerWidth();
										});
					
					horizontalTrackWidth 	-= verticalTrackWidth;
					
					paneHeight 				-= verticalTrackWidth;
					paneWidth 				-= horizontalTrackHeight;
					
					horizontalTrack	.parent()
									.append( 
										$('<div class="jspCorner" />').css('width', horizontalTrackHeight + 'px') 
									);
					
					sizeVerticalScrollbar();
					sizeHorizontalScrollbar();
				
				} // END if ( isScrollableH && isScrollableV )
				
				// reflow content
				if ( isScrollableH ) {
					pane.width((container.outerWidth() - originalPaddingTotalWidth) + 'px');
				} // END if ( isScrollableH )

				contentHeight 	= pane.outerHeight();
				percentInViewV 	= contentHeight / paneHeight;

				if ( isScrollableH ) {
					
					horizontalDragWidth = Math.ceil(1 / percentInViewH * horizontalTrackWidth);
					
					if (horizontalDragWidth > settings.horizontalDragMaxWidth) {
						horizontalDragWidth = settings.horizontalDragMaxWidth;
					} else if (horizontalDragWidth < settings.horizontalDragMinWidth) {
						horizontalDragWidth = settings.horizontalDragMinWidth;
					} // END IF
					
					horizontalDrag.width(horizontalDragWidth + 'px');
					dragMaxX = horizontalTrackWidth - horizontalDragWidth;
					_positionDragX(horizontalDragPosition); // To update the state for the arrow buttons
				
				} // END if ( isScrollableH )
				
				if ( isScrollableV ) {
				
					verticalDragHeight = Math.ceil(1 / percentInViewV * verticalTrackHeight);
					
					if (verticalDragHeight > settings.verticalDragMaxHeight) {
						verticalDragHeight = settings.verticalDragMaxHeight;
					} else if (verticalDragHeight < settings.verticalDragMinHeight) {
						verticalDragHeight = settings.verticalDragMinHeight;
					} // END IF
					
					verticalDrag.height(verticalDragHeight + 'px');
					
					dragMaxY = verticalTrackHeight - verticalDragHeight;
					
					_positionDragY(verticalDragPosition); // To update the state for the arrow buttons
				
				} // END if ( isScrollableV )
			
			} // END function resizeScrollbars()

			function appendArrows(	ele, p, a1, a2 ) {
				var p1 = "before", 
					p2 = "after", 
					aTemp;
				
				// Sniff for mac... Is there a better way to determine whether the arrows would naturally appear
				// at the top or the bottom of the bar?
				if (p == "os") {
					p = /Mac/.test(navigator.platform) ? "after" : "split";
				} // END IF
				
				if ( p == p1 ) {
					p2 		= p;
				} else if ( p == p2 ) {
					p1 		= p;
					aTemp 	= a1;
					a1 		= a2;
					a2 		= aTemp;
				} // END IF

				ele[p1](a1)[p2](a2);
			
			} // END function appendArrows

			function getArrowScroll(dirX, dirY, ele)
			{
				return function()
				{
					arrowScroll(dirX, dirY, this, ele);
					this.blur();
					return false;
				};
			}

			function arrowScroll ( dirX, dirY, arrow, ele ) {
				arrow = $(arrow).addClass('jspActive');

				var eve,
					scrollTimeout,
					isFirst		= 	true,
					doScroll 	= 	function() {
									
										if (dirX !== 0) {
											jsp.scrollByX(dirX * settings.arrowButtonSpeed);
										} // END IF
										
										if (dirY !== 0) {
											jsp.scrollByY(dirY * settings.arrowButtonSpeed);
										} // END IF
										
										scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.arrowRepeatFreq);
										
										isFirst = false;
									
									};

				doScroll();

				eve = ele ? 'mouseout.jsp' : 'mouseup.jsp';
				
				ele = ele || $('html');
				
				ele	.bind( eve, 	function() {
										arrow.removeClass('jspActive');
										scrollTimeout && clearTimeout(scrollTimeout);
										scrollTimeout = null;
										ele.unbind(eve);
										focusElem();
									}
					);
			
			} // END function arrowScroll ( dirX, dirY, arrow, ele )

			function initClickOnTrack()
			{
				removeClickOnTrack();
				if (isScrollableV) {
					verticalTrack.bind(
						'mousedown.jsp',
						function(e)
						{
							if (e.originalTarget === undefined || e.originalTarget == e.currentTarget) {
								var clickedTrack = $(this),
									offset = clickedTrack.offset(),
									direction = e.pageY - offset.top - verticalDragPosition,
									scrollTimeout,
									isFirst = true,
									doScroll = function()
									{
										var offset = clickedTrack.offset(),
											pos = e.pageY - offset.top - verticalDragHeight / 2,
											contentDragY = paneHeight * settings.scrollPagePercent,
											dragY = dragMaxY * contentDragY / (contentHeight - paneHeight);
										if (direction < 0) {
											if (verticalDragPosition - dragY > pos) {
												jsp.scrollByY(-contentDragY);
											} else {
												positionDragY(pos);
											}
										} else if (direction > 0) {
											if (verticalDragPosition + dragY < pos) {
												jsp.scrollByY(contentDragY);
											} else {
												positionDragY(pos);
											}
										} else {
											cancelClick();
											return;
										}
										scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.trackClickRepeatFreq);
										isFirst = false;
									},
									cancelClick = function()
									{
										scrollTimeout && clearTimeout(scrollTimeout);
										scrollTimeout = null;
										$(document).unbind('mouseup.jsp', cancelClick);
										focusElem();
									};
								doScroll();
								$(document).bind('mouseup.jsp', cancelClick);
								return false;
							}
						}
					);
				}
				
				if (isScrollableH) {
					horizontalTrack.bind(
						'mousedown.jsp',
						function(e)
						{
							if (e.originalTarget === undefined || e.originalTarget == e.currentTarget) {
								var clickedTrack = $(this),
									offset = clickedTrack.offset(),
									direction = e.pageX - offset.left - horizontalDragPosition,
									scrollTimeout,
									isFirst = true,
									doScroll = function()
									{
										var offset = clickedTrack.offset(),
											pos = e.pageX - offset.left - horizontalDragWidth / 2,
											contentDragX = paneWidth * settings.scrollPagePercent,
											dragX = dragMaxX * contentDragX / (contentWidth - paneWidth);
										if (direction < 0) {
											if (horizontalDragPosition - dragX > pos) {
												jsp.scrollByX(-contentDragX);
											} else {
												positionDragX(pos);
											}
										} else if (direction > 0) {
											if (horizontalDragPosition + dragX < pos) {
												jsp.scrollByX(contentDragX);
											} else {
												positionDragX(pos);
											}
										} else {
											cancelClick();
											return;
										}
										scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.trackClickRepeatFreq);
										isFirst = false;
									},
									cancelClick = function()
									{
										scrollTimeout && clearTimeout(scrollTimeout);
										scrollTimeout = null;
										$(document).unbind('mouseup.jsp', cancelClick);
										focusElem();
									};
								doScroll();
								$(document).bind('mouseup.jsp', cancelClick);
								return false;
							}
						}
					);
				}
			}

			function removeClickOnTrack() {
				if ( horizontalTrack ) {
					horizontalTrack.unbind('mousedown.jsp');
				}
				if ( verticalTrack ) {
					verticalTrack.unbind('mousedown.jsp');
				}
			} // END function removeClickOnTrack()

			function cancelDrag()
			{
				$('html').unbind('dragstart.jsp selectstart.jsp mousemove.jsp mouseup.jsp mouseleave.jsp');

				if (verticalDrag) {
					verticalDrag.removeClass('jspActive');
				}
				if (horizontalDrag) {
					horizontalDrag.removeClass('jspActive');
				}
				focusElem();
			}

			function positionDragY(destY, animate)
			{
				if (!isScrollableV) {
					return;
				}
				if (destY < 0) {
					destY = 0;
				} else if (destY > dragMaxY) {
					destY = dragMaxY;
				}

				// can't just check if(animate) because false is a valid value that could be passed in...
				if (animate === undefined) {
					animate = settings.animateScroll;
				}
				if (animate) {
					jsp.animate(verticalDrag, 'top', destY,	_positionDragY);
				} else {
					verticalDrag.css('top', destY);
					_positionDragY(destY);
				}

			}

			function _positionDragY(destY) {
			
				if ( destY === undefined ) {
					destY = verticalDrag.position().top;
				} // END IF
				
				container.scrollTop(0);
				verticalDragPosition = destY;

				var isAtTop 		= verticalDragPosition === 0,
					isAtBottom 		= verticalDragPosition == dragMaxY,
					percentScrolled = destY/ dragMaxY,
					destTop 		= -percentScrolled * (contentHeight - paneHeight);
				
				if ( wasAtTop != isAtTop || wasAtBottom != isAtBottom ) {
					wasAtTop = isAtTop;
					wasAtBottom = isAtBottom;
					elem.trigger('jsp-arrow-change', [wasAtTop, wasAtBottom, wasAtLeft, wasAtRight]);
				} // END IF
				
				updateVerticalArrows(isAtTop, isAtBottom);
				
				pane.css('top', destTop);
				
				elem.trigger('jsp-scroll-y', [-destTop, isAtTop, isAtBottom]).trigger('scroll');
			
			}

			function positionDragX(destX, animate)
			{
				if (!isScrollableH) {
					return;
				}
				if (destX < 0) {
					destX = 0;
				} else if (destX > dragMaxX) {
					destX = dragMaxX;
				}

				if (animate === undefined) {
					animate = settings.animateScroll;
				}
				if (animate) {
					jsp.animate(horizontalDrag, 'left', destX,	_positionDragX);
				} else {
					horizontalDrag.css('left', destX);
					_positionDragX(destX);
				}
			}

			function _positionDragX(destX)
			{
				if (destX === undefined) {
					destX = horizontalDrag.position().left;
				}

				container.scrollTop(0);
				horizontalDragPosition = destX;

				var isAtLeft = horizontalDragPosition === 0,
					isAtRight = horizontalDragPosition == dragMaxX,
					percentScrolled = destX / dragMaxX,
					destLeft = -percentScrolled * (contentWidth - paneWidth);

				if (wasAtLeft != isAtLeft || wasAtRight != isAtRight) {
					wasAtLeft = isAtLeft;
					wasAtRight = isAtRight;
					elem.trigger('jsp-arrow-change', [wasAtTop, wasAtBottom, wasAtLeft, wasAtRight]);
				}
				
				updateHorizontalArrows(isAtLeft, isAtRight);
				pane.css('left', destLeft);
				elem.trigger('jsp-scroll-x', [-destLeft, isAtLeft, isAtRight]).trigger('scroll');
			}

			function updateVerticalArrows(isAtTop, isAtBottom)
			{
				if (settings.showArrows) {
					arrowUp[isAtTop ? 'addClass' : 'removeClass']('jspDisabled');
					arrowDown[isAtBottom ? 'addClass' : 'removeClass']('jspDisabled');
				}
			}

			function updateHorizontalArrows(isAtLeft, isAtRight)
			{
				if (settings.showArrows) {
					arrowLeft[isAtLeft ? 'addClass' : 'removeClass']('jspDisabled');
					arrowRight[isAtRight ? 'addClass' : 'removeClass']('jspDisabled');
				}
			}

			function scrollToY(destY, animate)
			{
				var percentScrolled = destY / (contentHeight - paneHeight);
				positionDragY(percentScrolled * dragMaxY, animate);
			}

			function scrollToX(destX, animate)
			{
				var percentScrolled = destX / (contentWidth - paneWidth);
				positionDragX(percentScrolled * dragMaxX, animate);
			}

			function scrollToElement(ele, stickToTop, animate) {
			
				var e, 
					eleHeight, 
					eleWidth, 
					eleTop 	= 0, 
					eleLeft = 0, 
					viewportTop, 
					maxVisibleEleTop, 
					maxVisibleEleLeft, 
					destY, 
					destX;

				// Legal hash values aren't necessarily legal jQuery selectors so we need to catch any
				// errors from the lookup...
				try {
					e = $(ele);
				} catch (err) {
					return;
				} // END TRY
				
				eleHeight	= e.outerHeight();
				eleWidth	= e.outerWidth();

				container.scrollTop(0);
				container.scrollLeft(0);
				
				// loop through parents adding the offset top of any elements that are relatively positioned between
				// the focused element and the jspPane so we can get the true distance from the top
				// of the focused element to the top of the scrollpane...
				while (!e.is('.jspPane')) {
					eleTop += e.position().top;
					eleLeft += e.position().left;
					e = e.offsetParent();
					if (/^body|html$/i.test(e[0].nodeName)) {
						// we ended up too high in the document structure. Quit!
						return;
					}
				} // END WHILE

				viewportTop = contentPositionY();
				
				maxVisibleEleTop = viewportTop + paneHeight;
				
				if ( eleTop < viewportTop || stickToTop ) { // element is above viewport
					destY = eleTop - settings.verticalGutter;
				} else if ( eleTop + eleHeight > maxVisibleEleTop ) { // element is below viewport
					destY = eleTop - paneHeight + eleHeight + settings.verticalGutter;
				} // END IF
				
				if ( destY ) {
					scrollToY(destY, animate);
				} // END IF
				
				viewportLeft		= contentPositionX();
	            maxVisibleEleLeft	= viewportLeft + paneWidth;
				
	            if ( eleLeft < viewportLeft || stickToTop ) { // element is to the left of viewport
	                destX = eleLeft - settings.horizontalGutter;
	            } else if ( eleLeft + eleWidth > maxVisibleEleLeft ) { // element is to the right viewport
	                destX = eleLeft - paneWidth + eleWidth + settings.horizontalGutter;
	            } // END IF
				
	            if ( destX ) {
	                scrollToX(destX, animate);
	            } // END IF

			} // END function scrollToElement(ele, stickToTop, animate)

			function contentPositionX() {
				return -pane.position().left;
			}

			function contentPositionY() {
				return -pane.position().top;
			}

			function initMousewheel()
			{
				container.unbind(mwEvent).bind(
					mwEvent,
					function (event, delta, deltaX, deltaY) {
						var dX = horizontalDragPosition, dY = verticalDragPosition;
						jsp.scrollBy(deltaX * settings.mouseWheelSpeed, -deltaY * settings.mouseWheelSpeed, false);
						// return true if there was no movement so rest of screen can scroll
						return dX == horizontalDragPosition && dY == verticalDragPosition;
					}
				);
			}

			function removeMousewheel() {
				container.unbind(mwEvent);
			} // END function removeMousewheel()

			function nil()
			{
				return false;
			}

			function initFocusHandler()
			{
				pane.find(':input,a').unbind('focus.jsp').bind(
					'focus.jsp',
					function(e)
					{
						scrollToElement(e.target, false);
					}
				);
			}

			function removeFocusHandler() {
				pane.find(':input,a').unbind('focus.jsp');
			} // END function removeFocusHandler()
			
			function initKeyboardNav()
			{
				var keyDown, elementHasScrolled;
				// IE also focuses elements that don't have tabindex set.
				pane.focus(
					function()
					{
						elem.focus();
					}
				);
				
				elem.attr('tabindex', 0)
					.unbind('keydown.jsp keypress.jsp')
					.bind(
						'keydown.jsp',
						function(e)
						{
							if (e.target !== this){
								return;
							}
							var dX = horizontalDragPosition, dY = verticalDragPosition;
							switch(e.keyCode) {
								case 40: // down
								case 38: // up
								case 34: // page down
								case 32: // space
								case 33: // page up
								case 39: // right
								case 37: // left
									keyDown = e.keyCode;
									keyDownHandler();
									break;
								case 35: // end
									scrollToY(contentHeight - paneHeight);
									keyDown = null;
									break;
								case 36: // home
									scrollToY(0);
									keyDown = null;
									break;
							}

							elementHasScrolled = e.keyCode == keyDown && dX != horizontalDragPosition || dY != verticalDragPosition;
							return !elementHasScrolled;
						}
					).bind(
						'keypress.jsp', // For FF/ OSX so that we can cancel the repeat key presses if the JSP scrolls...
						function(e)
						{
							if (e.keyCode == keyDown) {
								keyDownHandler();
							}
							return !elementHasScrolled;
						}
					);
				
				if (settings.hideFocus) {
					elem.css('outline', 'none');
					if ('hideFocus' in container[0]){
						elem.attr('hideFocus', true);
					}
				} else {
					elem.css('outline', '');
					if ('hideFocus' in container[0]){
						elem.attr('hideFocus', false);
					}
				}
				
				function keyDownHandler()
				{
					var dX = horizontalDragPosition, dY = verticalDragPosition;
					switch(keyDown) {
						case 40: // down
							jsp.scrollByY(settings.keyboardSpeed, false);
							break;
						case 38: // up
							jsp.scrollByY(-settings.keyboardSpeed, false);
							break;
						case 34: // page down
						case 32: // space
							jsp.scrollByY(paneHeight * settings.scrollPagePercent, false);
							break;
						case 33: // page up
							jsp.scrollByY(-paneHeight * settings.scrollPagePercent, false);
							break;
						case 39: // right
							jsp.scrollByX(settings.keyboardSpeed, false);
							break;
						case 37: // left
							jsp.scrollByX(-settings.keyboardSpeed, false);
							break;
					}

					elementHasScrolled = dX != horizontalDragPosition || dY != verticalDragPosition;
					return elementHasScrolled;
				}
			}
			
			function removeKeyboardNav() {
				elem.attr('tabindex', '-1')
					.removeAttr('tabindex')
					.unbind('keydown.jsp keypress.jsp');
			} // END function removeKeyboardNav()

			function observeHash() {
			
				if ( location.hash && location.hash.length > 1 ) {
					var e, 
						retryInt;
					
					try {
						e = $(location.hash);
					} catch (err) {
						return;
					} // END try

					if ( e.length && pane.find(location.hash) ) {
						// nasty workaround but it appears to take a little while before the hash has done its thing
						// to the rendered page so we just wait until the container's scrollTop has been messed up.
						if (container.scrollTop() === 0) {
						
							retryInt = setInterval( function() {
														if ( container.scrollTop() > 0 ) {
															scrollToElement(location.hash, true);
															$(document).scrollTop(container.position().top);
															clearInterval(retryInt);
														}
													}, 50 );
						} else {
							scrollToElement(location.hash, true);
							$(document).scrollTop(container.position().top);
						
						} // END if (container.scrollTop() === 0)
					
					} // END if ( e.length && pane.find(location.hash) )
				
				} // END if ( location.hash && location.hash.length > 1 )
			
			} // END function observeHash()

			function unhijackInternalLinks() {
				$('a.jspHijack').unbind('click.jsp-hijack').removeClass('jspHijack');
			} // END function unhijackInternalLinks()

			function hijackInternalLinks() {
				unhijackInternalLinks();
				$('a[href^=#]')	.addClass('jspHijack')
								.bind( 'click.jsp-hijack', 	function() {
																var uriParts = this.href.split('#'), 
																	hash;
																if ( uriParts.length > 1 ) {
																	hash = uriParts[1];
																	if ( hash.length > 0 && pane.find('#' + hash).length > 0 ) {
																		scrollToElement('#' + hash, true);
																		// Need to return false otherwise things mess up... Would be nice to maybe also scroll
																		// the window to the top of the scrollpane?
																		return false;
																	} // end if
																} // end if
															} // end FUNCTION
								);
			} // END function hijackInternalLinks()
			
			// If no element has focus, focus elem to support keyboard navigation
			function focusElem()
			{
				if (!$(':focus').length) {
					elem.focus();
				}
			}
			
			// Init touch on iPad, iPhone, iPod, Android
			function initTouch()
			{
				var startX,
					startY,
					touchStartX,
					touchStartY,
					moved,
					moving = false;
  
				container.unbind('touchstart.jsp touchmove.jsp touchend.jsp click.jsp-touchclick').bind(
					'touchstart.jsp',
					function(e)
					{
						var touch = e.originalEvent.touches[0];
						startX = contentPositionX();
						startY = contentPositionY();
						touchStartX = touch.pageX;
						touchStartY = touch.pageY;
						moved = false;
						moving = true;
					}
				).bind(
					'touchmove.jsp',
					function(ev)
					{
						if(!moving) {
							return;
						}
						
						var touchPos = ev.originalEvent.touches[0],
							dX = horizontalDragPosition, dY = verticalDragPosition;
						
						jsp.scrollTo(startX + touchStartX - touchPos.pageX, startY + touchStartY - touchPos.pageY);
						
						moved = moved || Math.abs(touchStartX - touchPos.pageX) > 5 || Math.abs(touchStartY - touchPos.pageY) > 5;
						
						// return true if there was no movement so rest of screen can scroll
						return dX == horizontalDragPosition && dY == verticalDragPosition;
					}
				).bind(
					'touchend.jsp',
					function(e)
					{
						moving = false;
						/*if(moved) {
							return false;
						}*/
					}
				).bind(
					'click.jsp-touchclick',
					function(e)
					{
						if(moved) {
							moved = false;
							return false;
						}
					}
				);
			}
			
			function destroy(){
				var currentY = contentPositionY(),
					currentX = contentPositionX();
				elem.removeClass('jspScrollable').unbind('.jsp');
				elem.replaceWith(originalElement.append(pane.children()));
				originalElement.scrollTop(currentY);
				originalElement.scrollLeft(currentX);
			}

			// Public API
			$.extend(
				jsp,
				{
					// Reinitialises the scroll pane (if it's internal dimensions have changed since the last time it
					// was initialised). The settings object which is passed in will override any settings from the
					// previous time it was initialised - if you don't pass any settings then the ones from the previous
					// initialisation will be used.
					reinitialise: function(s)
					{
						s = $.extend({}, settings, s);
						initialise(s);
					},
					// Scrolls the specified element (a jQuery object, DOM node or jQuery selector string) into view so
					// that it can be seen within the viewport. If stickToTop is true then the element will appear at
					// the top of the viewport, if it is false then the viewport will scroll as little as possible to
					// show the element. You can also specify if you want animation to occur. If you don't provide this
					// argument then the animateScroll value from the settings object is used instead.
					scrollToElement: function(ele, stickToTop, animate)
					{
						scrollToElement(ele, stickToTop, animate);
					},
					// Scrolls the pane so that the specified co-ordinates within the content are at the top left
					// of the viewport. animate is optional and if not passed then the value of animateScroll from
					// the settings object this jScrollPane was initialised with is used.
					scrollTo: function(destX, destY, animate)
					{
						scrollToX(destX, animate);
						scrollToY(destY, animate);
					},
					// Scrolls the pane so that the specified co-ordinate within the content is at the left of the
					// viewport. animate is optional and if not passed then the value of animateScroll from the settings
					// object this jScrollPane was initialised with is used.
					scrollToX: function(destX, animate)
					{
						scrollToX(destX, animate);
					},
					// Scrolls the pane so that the specified co-ordinate within the content is at the top of the
					// viewport. animate is optional and if not passed then the value of animateScroll from the settings
					// object this jScrollPane was initialised with is used.
					scrollToY: function(destY, animate)
					{
						scrollToY(destY, animate);
					},
					// Scrolls the pane to the specified percentage of its maximum horizontal scroll position. animate
					// is optional and if not passed then the value of animateScroll from the settings object this
					// jScrollPane was initialised with is used.
					scrollToPercentX: function(destPercentX, animate)
					{
						scrollToX(destPercentX * (contentWidth - paneWidth), animate);
					},
					// Scrolls the pane to the specified percentage of its maximum vertical scroll position. animate
					// is optional and if not passed then the value of animateScroll from the settings object this
					// jScrollPane was initialised with is used.
					scrollToPercentY: function(destPercentY, animate)
					{
						scrollToY(destPercentY * (contentHeight - paneHeight), animate);
					},
					// Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then
					// the value of animateScroll from the settings object this jScrollPane was initialised with is used.
					scrollBy: function(deltaX, deltaY, animate)
					{
						jsp.scrollByX(deltaX, animate);
						jsp.scrollByY(deltaY, animate);
					},
					// Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then
					// the value of animateScroll from the settings object this jScrollPane was initialised with is used.
					scrollByX: function(deltaX, animate)
					{
						var destX = contentPositionX() + deltaX,
							percentScrolled = destX / (contentWidth - paneWidth);
						positionDragX(percentScrolled * dragMaxX, animate);
					},
					
					// Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then
					// the value of animateScroll from the settings object this jScrollPane was initialised with is used.
					scrollByY: function(deltaY, animate) {
						
						var destY = contentPositionY() + deltaY,
							percentScrolled = destY / (contentHeight - paneHeight);
						
						positionDragY(percentScrolled * dragMaxY, animate);
					
					}, // END FUNCTION scrollByY()@???
					
					// Positions the horizontal drag at the specified x position (and updates the viewport to reflect
					// this). animate is optional and if not passed then the value of animateScroll from the settings
					// object this jScrollPane was initialised with is used.
					positionDragX: function(x, animate)
					{
						positionDragX(x, animate);
					},
					// Positions the vertical drag at the specified y position (and updates the viewport to reflect
					// this). animate is optional and if not passed then the value of animateScroll from the settings
					// object this jScrollPane was initialised with is used.
					positionDragY: function(y, animate)
					{
						positionDragX(y, animate);
					},
					// This method is called when jScrollPane is trying to animate to a new position. You can override
					// it if you want to provide advanced animation functionality. It is passed the following arguments:
					//  * ele          - the element whose position is being animated
					//  * prop         - the property that is being animated
					//  * value        - the value it's being animated to
					//  * stepCallback - a function that you must execute each time you update the value of the property
					// You can use the default implementation (below) as a starting point for your own implementation.
					animate: function(ele, prop, value, stepCallback)
					{
						var params = {};
						params[prop] = value;
						ele.animate(
							params,
							{
								'duration'	: settings.animateDuration,
								'ease'		: settings.animateEase,
								'queue'		: false,
								'step'		: stepCallback
							}
						);
					},
					// Returns the current x position of the viewport with regards to the content pane.
					getContentPositionX: function()
					{
						return contentPositionX();
					},
					
					// Returns the current y position of the viewport with regards to the content pane.
					getContentPositionY: function() {
						return contentPositionY();
					}, // END FUNCTION getContentPositionY()@???
					
					// Returns the width of the content within the scroll pane.
					getContentWidth: function()
					{
						return contentWidth();
					},
					// Returns the height of the content within the scroll pane.
					getContentHeight: function()
					{
						return contentHeight();
					},
					// Returns the horizontal position of the viewport within the pane content.
					getPercentScrolledX: function()
					{
						return contentPositionX() / (contentWidth - paneWidth);
					},
					// Returns the vertical position of the viewport within the pane content.
					getPercentScrolledY: function()
					{
						return contentPositionY() / (contentHeight - paneHeight);
					},
					// Returns whether or not this scrollpane has a horizontal scrollbar.
					getIsScrollableH: function()
					{
						return isScrollableH;
					},
					// Returns whether or not this scrollpane has a vertical scrollbar.
					getIsScrollableV: function()
					{
						return isScrollableV;
					},
					// Gets a reference to the content pane. It is important that you use this method if you want to
					// edit the content of your jScrollPane as if you access the element directly then you may have some
					// problems (as your original element has had additional elements for the scrollbars etc added into
					// it).
					getContentPane: function()
					{
						return pane;
					},
					// Scrolls this jScrollPane down as far as it can currently scroll. If animate isn't passed then the
					// animateScroll value from settings is used instead.
					scrollToBottom: function(animate)
					{
						positionDragY(dragMaxY, animate);
					},
					// Hijacks the links on the page which link to content inside the scrollpane. If you have changed
					// the content of your page (e.g. via AJAX) and want to make sure any new anchor links to the
					// contents of your scroll pane will work then call this function.
					hijackInternalLinks: function()
					{
						hijackInternalLinks();
					},
					// Removes the jScrollPane and returns the page to the state it was in before jScrollPane was
					// initialised.
					destroy: function()
					{
							destroy();
					}
				}
			);
			
			initialise(s);
		} // END function JScrollPane(elem, s)

		// Pluginifying code...
		settings = $.extend({}, $.fn.jScrollPane.defaults, settings);
		
		// Apply default speed
		$.each(['mouseWheelSpeed', 'arrowButtonSpeed', 'trackClickSpeed', 'keyboardSpeed'], function() {
			settings[this] = settings[this] || settings.speed;
		});

		var ret;
		this.each(
			function()
			{
				var elem = $(this), jspApi = elem.data('jsp');
				if (jspApi) {
					jspApi.reinitialise(settings);
				} else {
					jspApi = new JScrollPane(elem, settings);
					elem.data('jsp', jspApi);
				}
				ret = ret ? ret.add(elem) : elem;
			}
		);
		return ret;
	};

	$.fn.jScrollPane.defaults = {
		showArrows					: true,
		maintainPosition			: true,
		clickOnTrack				: true,
		autoReinitialise			: false,
		autoReinitialiseDelay		: 500,
		verticalDragMinHeight		: 0,
		verticalDragMaxHeight		: 99999,
		horizontalDragMinWidth		: 0,
		horizontalDragMaxWidth		: 99999,
		animateScroll				: false,
		animateDuration				: 300,
		animateEase					: 'linear',
		hijackInternalLinks			: false,
		verticalGutter				: 4,
		horizontalGutter			: 4,
		mouseWheelSpeed				: 0,
		arrowButtonSpeed			: 0,
		arrowRepeatFreq				: 50,
		arrowScrollOnHover			: false,
		trackClickSpeed				: 0,
		trackClickRepeatFreq		: 70,
		verticalArrowPositions		: 'split',
		horizontalArrowPositions	: 'split',
		enableKeyboardNavigation	: true,
		hideFocus					: false,
		keyboardSpeed				: 0,
		initialDelay                : 300,      // Delay before starting repeating
		speed						: 30,		// Default speed when others falsey
		scrollPagePercent			: .8		// Percent of visible area scrolled when pageUp/Down or track area pressed
	};

})(jQuery,this);


