Personalizando barra de rolagem com jQuery

O desejo de muitos designers e desenvolvedores de personalizar barras de rolagem dentro de seus sites para que elas acompanhem a identidade visual de seus projetos agora pode ser solucionada facilmente. Durante muito tempo esse tipo de estilização era feita somente em sites desenvolvidos em Flash ou utilizando a os atributos proprietários scrollpanel, como o mestre Maujor mostra nesse artigo, mas que funcionavam somente no Internet Explorer.

Nesse artigo utilizaremos um plugin Jquery, o jScrollPane que permite a estilização de suas barrinhas de maneira fácil e rápida.

Para utilizar o plugin, você precisará dos seguintes arquivos:

  • jquery.jscrollpane.css – Css básico aonde será setado a personalização da barra
/*
* CSS Styles that are needed by jScrollPane for it to operate correctly.
*
* Include this stylesheet in your site or copy and paste the styles below into your stylesheet - jScrollPane
* may not operate correctly without them.
*/

.jspContainer
{
overflow: hidden;
position: relative;
}

.jspPane
{
position: absolute;
}

.jspVerticalBar
{
position: absolute;
top: 0;
right: 0;
width: 16px;
height: 100%;
background: red;
}

.jspHorizontalBar
{
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 16px;
background: red;
}

.jspVerticalBar *,
.jspHorizontalBar *
{
margin: 0;
padding: 0;
}

.jspCap
{
display: none;
}

.jspHorizontalBar .jspCap
{
float: left;
}

.jspTrack
{
background: #dde;
position: relative;
}

.jspDrag
{
background: #bbd;
position: relative;
top: 0;
left: 0;
cursor: pointer;
}

.jspHorizontalBar .jspTrack,
.jspHorizontalBar .jspDrag
{
float: left;
height: 100%;
}

.jspArrow
{
background: #50506d;
text-indent: -20000px;
display: block;
cursor: pointer;
}

.jspArrow.jspDisabled
{
cursor: default;
background: #80808d;
}

.jspVerticalBar .jspArrow
{
height: 16px;
}

.jspHorizontalBar .jspArrow
{
width: 16px;
float: left;
height: 100%;
}

.jspVerticalBar .jspArrow:focus
{
outline: none;
}

.jspCorner
{
background: #eeeef4;
float: left;
height: 100%;
}

/* Yuk! CSS Hack for IE6 3 pixel bug 🙁 */
* html .jspCorner
{
margin: 0 -3px 0 0;
}
  • jquery.jscrollpane.min.js – O Plugin jScrollPane.
/*!
* jScrollPane - v2.0.0beta11 - 2011-07-04
* 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.0beta11, Last updated: 2011-07-04*
//
// 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) 2011 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.0beta11 - (in progress)
// 2.0.0beta10 - (2011-04-17) cleaner required size calculation, improved keyboard support, stickToBottom/Left, other small fixes
// 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(false, false).empty(),
mwEvent = $.fn.mwheelIntent ? 'mwheelIntent.jsp' : 'mousewheel.jsp';

originalPadding = elem.css('paddingTop') + ' ' +
elem.css('paddingRight') + ' ' +
elem.css('paddingBottom') + ' ' +
elem.css('paddingLeft');
originalPaddingTotalWidth = (parseInt(elem.css('paddingLeft'), 10) || 0) +
(parseInt(elem.css('paddingRight'), 10) || 0);

function initialise(s)
{

var /*firstChild, lastChild, */isMaintainingPositon, lastContentX, lastContentY,
hasContainingSpaceChanged, originalScrollTop, originalScrollLeft,
maintainAtBottom = false, maintainAtRight = false;

settings = s;

if (pane === undefined) {
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;
paneHeight = elem.innerHeight();

elem.width(paneWidth);

pane = $('

 
').css('padding', originalPadding).append(elem.children());
container = $('

 
')
.css({
'width': paneWidth + 'px',
'height': paneHeight + 'px'
}
).append(pane).appendTo(elem);

/*
// 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 {
elem.css('width', '');

maintainAtBottom = settings.stickToBottom && isCloseToBottom();
maintainAtRight = settings.stickToRight && isCloseToRight();

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'
});
}

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

pane.css('width', '');
elem.width(paneWidth);

container.find('>.jspVerticalBar,>.jspHorizontalBar').remove().end();
}

pane.css('overflow', 'auto');
if (s.contentWidth) {
contentWidth = s.contentWidth;
} else {
contentWidth = pane[0].scrollWidth;
}
contentHeight = pane[0].scrollHeight;
pane.css('overflow', '');

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)) {
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();
}

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

if (isMaintainingPositon) {
scrollToX(maintainAtRight ? (contentWidth - paneWidth ) : lastContentX, false);
scrollToY(maintainAtBottom ? (contentHeight - paneHeight) : lastContentY, false);
}

initFocusHandler();
initMousewheel();
initTouch();

if (settings.enableKeyboardNavigation) {
initKeyboardNav();
}
if (settings.clickOnTrack) {
initClickOnTrack();
}

observeHash();
if (settings.hijackInternalLinks) {
hijackInternalLinks();
}
}

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

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

elem.trigger('jsp-initialised', [isScrollableH || isScrollableV]);
}

function initialiseVerticalScroll()
{
if (isScrollableV) {

container.append(
$('

 
').append(
$('

 
'),
$('

 
').append(
$('

 
').append(
$('

 
'),
$('

 
')
)
),
$('

 
')
)
);

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

if (settings.showArrows) {
arrowUp = $('').bind(
'mousedown.jsp', getArrowScroll(0, -1)
).bind('click.jsp', nil);
arrowDown = $('').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);
}

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();
}
}

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)
try {
if (verticalBar.position().left === 0) {
pane.css('margin-left', scrollbarWidth + 'px');
}
} catch (err) {
}
}

function initialiseHorizontalScroll()
{
if (isScrollableH) {

container.append(
$('

 
').append(
$('

 
'),
$('

 
').append(
$('

 
').append(
$('

 
'),
$('

 
')
)
),
$('

 
')
)
);

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

if (settings.showArrows) {
arrowLeft = $('').bind(
'mousedown.jsp', getArrowScroll(-1, 0)
).bind('click.jsp', nil);
arrowRight = $('').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));
}
appendArrows(horizontalTrack, settings.horizontalArrowPositions, arrowLeft, arrowRight);
}

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();
}
}

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(
$('

 
').css('width', horizontalTrackHeight + 'px')
);
sizeVerticalScrollbar();
sizeHorizontalScrollbar();
}
// reflow content
if (isScrollableH) {
pane.width((container.outerWidth() - originalPaddingTotalWidth) + 'px');
}
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;
}
horizontalDrag.width(horizontalDragWidth + 'px');
dragMaxX = horizontalTrackWidth - horizontalDragWidth;
_positionDragX(horizontalDragPosition); // To update the state for the arrow buttons
}
if (isScrollableV) {
verticalDragHeight = Math.ceil(1 / percentInViewV * verticalTrackHeight);
if (verticalDragHeight > settings.verticalDragMaxHeight) {
verticalDragHeight = settings.verticalDragMaxHeight;
} else if (verticalDragHeight < settings.verticalDragMinHeight) {
verticalDragHeight = settings.verticalDragMinHeight;
}
verticalDrag.height(verticalDragHeight + 'px');
dragMaxY = verticalTrackHeight - verticalDragHeight;
_positionDragY(verticalDragPosition); // To update the state for the arrow buttons
}
}

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";
}
if (p == p1) {
p2 = p;
} else if (p == p2) {
p1 = p;
aTemp = a1;
a1 = a2;
a2 = aTemp;
}

ele[p1](a1)[p2](a2);
}

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);
}
if (dirY !== 0) {
jsp.scrollByY(dirY * settings.arrowButtonSpeed);
}
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 &amp;amp;&amp;amp; clearTimeout(scrollTimeout);
scrollTimeout = null;
ele.unbind(eve);
}
);
}

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 &amp;amp;&amp;amp; clearTimeout(scrollTimeout);
scrollTimeout = null;
$(document).unbind('mouseup.jsp', cancelClick);
};
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 &amp;amp;&amp;amp; clearTimeout(scrollTimeout);
scrollTimeout = null;
$(document).unbind('mouseup.jsp', cancelClick);
};
doScroll();
$(document).bind('mouseup.jsp', cancelClick);
return false;
}
}
);
}
}

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

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

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

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;
}

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]);
}

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, viewportLeft, 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;
}
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;
}
}

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;
}
if (destY) {
scrollToY(destY, animate);
}

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;
}
if (destX) {
scrollToX(destX, animate);
}

}

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

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

function isCloseToBottom()
{
var scrollableHeight = contentHeight - paneHeight;
return (scrollableHeight > 20) &amp;amp;&amp;amp; (scrollableHeight - contentPositionY() < 10);
}

function isCloseToRight()
{
var scrollableWidth = contentWidth - paneWidth;
return (scrollableWidth > 20) &amp;amp;&amp;amp; (scrollableWidth - contentPositionX() < 10);
}

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 &amp;amp;&amp;amp; dY == verticalDragPosition;
}
);
}

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

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');
}

function initKeyboardNav()
{
var keyDown, elementHasScrolled, validParents = [];
isScrollableH &amp;amp;&amp;amp; validParents.push(horizontalBar[0]);
isScrollableV &amp;amp;&amp;amp; validParents.push(verticalBar[0]);

// 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 &amp;amp;&amp;amp; !(validParents.length &amp;amp;&amp;amp; $(e.target).closest(validParents).length)){
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 &amp;amp;&amp;amp; 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');
}

function observeHash()
{
if (location.hash &amp;amp;&amp;amp; location.hash.length > 1) {
var e,
retryInt,
hash = escape(location.hash) // hash must be escaped to prevent XSS
;
try {
e = $(hash);
} catch (err) {
return;
}

if (e.length &amp;amp;&amp;amp; pane.find(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(hash, true);
$(document).scrollTop(container.position().top);
clearInterval(retryInt);
}
},
50
);
} else {
scrollToElement(hash, true);
$(document).scrollTop(container.position().top);
}
}
}
}

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

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 &amp;amp;&amp;amp; 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;
}
}
}
);
}

// 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 &amp;amp;&amp;amp; 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() + Math[deltaXpercentScrolled = 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() + Math[deltaYpercentScrolled = destY / (contentHeight - paneHeight);
positionDragY(percentScrolled * dragMaxY, animate);
},
// 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)
{
positionDragY(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,
'easing' : 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();
},
// 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);
}

// Pluginifying code...
settings = $.extend({}, $.fn.jScrollPane.defaults, settings);

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

return 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);
}
}
);
};

$.fn.jScrollPane.defaults = {
showArrows : false,
maintainPosition : true,
stickToBottom : false,
stickToRight : false,
clickOnTrack : true,
autoReinitialise : false,
autoReinitialiseDelay : 500,
verticalDragMinHeight : 0,
verticalDragMaxHeight : 99999,
horizontalDragMinWidth : 0,
horizontalDragMaxWidth : 99999,
contentWidth : undefined,
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);

E, logicamente, a biblioteca jQuery.!

Esses são os três arquivos fundamentais para que o seu plugin funcione corretamente. Contudo, o desenvolvedor recomenda o uso dos plugins MouseWheel e MwheelIntent, que permitirão ao usuário utilizar a barra de rolagem através da “rodinha do mouse”.

Na página onde o plugin será aplicado basta adicionar uma chamada para a função jScrollPane(). Não se esqueça de setar, no estilo do seu elemento, como será a rolagem: se será do tipo vertical, horizontal, ambas ou automática. Normalmente, para que as barras sejam exibidas somente se necessário, trabalhe com overflow:auto e seus problemas serão resolvidos!

$(function() {

$('#caixa').jScrollPane();
});

Caso você tenha alguma dúvida, consulte o site do desenvolvedor ou faça o download do meu exemplo, com todos os arquivos.

// Comente!

comentários

eufacoprogramas

Olá, eu sou a Gabi e eu criei o "Eu Faço Programas" em 2011, quando ainda trabalhava em desenvolvimento web. Atualmente meu trabalho é focado em estratégia digital e redes sociais. Quer saber mais? www.imgabi.com

// 1 Comentário

  • Responder janeiro 25, 2012

    Cristian Stroparo

    Oi, muito massa o plugin!
    Mas no code css e no javascript no final tem umas tags html que acho que nao deviam tar ali né? (sem escape, sem ver no browser). Valeu!

// Siga as boas práticas: Comente!