(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 = $('<div class="jspPane" />').css('padding', originalPadding).append(elem.children()); | |
container = $('<div class="jspContainer" />') | |
.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( | |
$('<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'); | |
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); | |
} | |
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( | |
$('<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)); | |
} | |
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( | |
$('<div class="jspCorner" />').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 && 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 && 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 && 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) && (scrollableHeight - contentPositionY() < 10); | |
} | |
function isCloseToRight() | |
{ | |
var scrollableWidth = contentWidth - paneWidth; | |
return (scrollableWidth > 20) && (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 && 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 && validParents.push(horizontalBar[0]); | |
isScrollableV && 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 && !(validParents.length && $(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 && 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 && 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 && 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 && 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 && 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); | |
// clear reinitialize timer if active | |
if (reinitialiseInterval) { | |
clearInterval(reinitialiseInterval); | |
} | |
} | |
// 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[deltaX<0 ? 'floor' : 'ceil'](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() + Math[deltaY<0 ? 'floor' : 'ceil'](deltaY), | |
percentScrolled = 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); | |