/* 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 47 2009-02-08 17:56:16Z 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(
$('').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(
$('').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth+'px'}).append(
$('').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth+'px'}).append(
$('').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth+'px'}),
$('').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(
$('')
.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),
$('')
.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;
$this.data('jScrollPanePosition', (paneHeight-contentHeight)*-p);
$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();
return false;
};
$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 maxScroll = contentHeight - paneHeight;
pos = pos > maxScroll ? maxScroll : pos;
$this.data('jScrollPaneMaxScroll', maxScroll);
var destDragPosition = pos/maxScroll * 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 && 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.jScrollPaneRemove = function()
{
$(this).each(function()
{
$this = $(this);
var $c = $this.parent();
if ($c.is('.jScrollPaneContainer')) {
$this.css(
{
'top':'',
'height':'',
'width':'',
'padding':'',
'overflow':'',
'position':''
}
);
$c.after($this).remove();
}
});
}
$.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