1063 lines
33 KiB
JavaScript
1063 lines
33 KiB
JavaScript
/*!
|
|
* jScrollPane - v2.0.0beta3 - 2010-08-27
|
|
* http://jscrollpane.kelvinluck.com/
|
|
*
|
|
* Copyright (c) 2010 Kelvin Luck
|
|
* Dual licensed under the MIT or GPL licenses.
|
|
*/
|
|
|
|
// Script: jScrollPane - cross browser customisable scrollbars
|
|
//
|
|
// *Version: 2.0.0beta3, Last updated: 2010-08-27*
|
|
//
|
|
// Project Home - http://jscrollpane.kelvinluck.com/
|
|
// GitHub - http://github.com/vitch/jScrollPane
|
|
// Source - http://github.com/vitch/jScrollPane/raw/master/script/jquery.jscrollpane.js
|
|
// (Minified) - http://github.com/vitch/jScrollPane/raw/master/script/jquery.jscrollpane.min.js
|
|
//
|
|
// About: License
|
|
//
|
|
// Copyright (c) 2010 Kelvin Luck
|
|
// Dual licensed under the MIT or GPL Version 2 licenses.
|
|
// http://jscrollpane.kelvinluck.com/MIT-LICENSE.txt
|
|
// http://jscrollpane.kelvinluck.com/GPL-LICENSE.txt
|
|
//
|
|
// About: Examples
|
|
//
|
|
// All examples and demos are available through the jScrollPane example site at:
|
|
// http://jscrollpane.kelvinluck.com/
|
|
//
|
|
// About: Support and Testing
|
|
//
|
|
// This plugin is tested on the browsers below and has been found to work reliably on them. If you run
|
|
// into a problem on one of the supported browsers then please visit the support section on the jScrollPane
|
|
// website (http://jscrollpane.kelvinluck.com/) for more information on getting support. You are also
|
|
// welcome to fork the project on GitHub if you can contribute a fix for a given issue.
|
|
//
|
|
// jQuery Versions - 1.4.2
|
|
// Browsers Tested - Firefox 3.6.8, Safari 5, Opera 10.6, Chrome 5.0, IE 6, 7, 8
|
|
//
|
|
// About: Release History
|
|
//
|
|
// 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, previousPaneWidth,
|
|
wasAtTop = true, wasAtLeft = true, wasAtBottom = false, wasAtRight = false,
|
|
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')) || 0) +
|
|
(parseInt(elem.css('paddingRight')) || 0);
|
|
|
|
initialise(s);
|
|
|
|
function initialise(s)
|
|
{
|
|
|
|
var clonedElem, tempWrapper, /*firstChild, lastChild, */isMaintainingPositon, lastContentX, lastContentY,
|
|
hasContainingSpaceChanged;
|
|
|
|
settings = s;
|
|
|
|
if (pane == undefined) {
|
|
|
|
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" />').wrap(
|
|
$('<div class="jspContainer" />')
|
|
.css({
|
|
'width': paneWidth + 'px',
|
|
'height': paneHeight + 'px'
|
|
}
|
|
)
|
|
);
|
|
|
|
elem.wrapInner(pane.parent());
|
|
// Need to get the vars after being added to the document, otherwise they reference weird
|
|
// disconnected orphan elements...
|
|
container = elem.find('>.jspContainer');
|
|
pane = container.find('>.jspPane');
|
|
pane.css('padding', originalPadding);
|
|
|
|
/*
|
|
// 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', null);
|
|
|
|
hasContainingSpaceChanged = elem.outerWidth() + originalPaddingTotalWidth != paneWidth || elem.outerHeight() != paneHeight;
|
|
|
|
if (hasContainingSpaceChanged) {
|
|
paneWidth = elem.innerWidth() + originalPaddingTotalWidth;
|
|
paneHeight = elem.innerHeight();
|
|
container.css({
|
|
'width': paneWidth + 'px',
|
|
'height': paneHeight + 'px'
|
|
});
|
|
}
|
|
|
|
previousPaneWidth = pane.innerWidth();
|
|
|
|
if (!hasContainingSpaceChanged && pane.outerWidth() == contentWidth && pane.outerHeight() == contentHeight) {
|
|
// Nothing has changed since we last initialised
|
|
if (isScrollableH || isScrollableV) { // If we had already set a width then re-set it
|
|
pane.css('width', previousPaneWidth + 'px');
|
|
elem.css('width', (previousPaneWidth + originalPaddingTotalWidth) + 'px');
|
|
}
|
|
// Then abort...
|
|
return;
|
|
}
|
|
|
|
pane.css('width', null);
|
|
elem.css('width', (paneWidth + originalPaddingTotalWidth) + 'px');
|
|
|
|
container.find('>.jspVerticalBar,>.jspHorizontalBar').remove().end();
|
|
}
|
|
|
|
// Unfortunately it isn't that easy to find out the width of the element as it will always report the
|
|
// width as allowed by its container, regardless of overflow settings.
|
|
// A cunning workaround is to clone the element, set its position to absolute and place it in a narrow
|
|
// container. Now it will push outwards to its maxium real width...
|
|
clonedElem = pane.clone().css('position', 'absolute');
|
|
tempWrapper = $('<div style="width:1px; position: relative;" />').append(clonedElem);
|
|
$('body').append(tempWrapper);
|
|
contentWidth = Math.max(pane.outerWidth(), clonedElem.outerWidth());
|
|
tempWrapper.remove();
|
|
|
|
contentHeight = pane.outerHeight(true);
|
|
/*alert(contentHeight); */
|
|
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() + 'px'
|
|
});
|
|
removeMousewheel();
|
|
removeFocusHandler();
|
|
removeKeyboardNav();
|
|
unhijackInternalLinks();
|
|
} else {
|
|
elem.addClass('jspScrollable');
|
|
|
|
isMaintainingPositon = settings.maintainPosition && (verticalDragPosition || horizontalDragPosition);
|
|
if (isMaintainingPositon) {
|
|
lastContentX = contentPositionX();
|
|
lastContentY = contentPositionY();
|
|
}
|
|
|
|
initialiseVerticalScroll();
|
|
initialiseHorizontalScroll();
|
|
resizeScrollbars();
|
|
|
|
if (isMaintainingPositon) {
|
|
scrollToX(lastContentX);
|
|
scrollToY(lastContentY);
|
|
}
|
|
|
|
initFocusHandler();
|
|
initMousewheel();
|
|
if (settings.enableKeyboardNavigation) {
|
|
initKeyboardNav();
|
|
}
|
|
|
|
observeHash();
|
|
if (settings.hijackInternalLinks) {
|
|
hijackInternalLinks();
|
|
}
|
|
}
|
|
|
|
if (settings.autoReinitialise && !reinitialiseInterval) {
|
|
reinitialiseInterval = setInterval(
|
|
function()
|
|
{
|
|
initialise(settings);
|
|
},
|
|
settings.autoReinitialiseDelay
|
|
);
|
|
} else if (!settings.autoReinitialise && reinitialiseInterval) {
|
|
clearInterval(reinitialiseInterval)
|
|
}
|
|
|
|
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', function() { return false; });
|
|
|
|
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();
|
|
updateVerticalArrows();
|
|
}
|
|
}
|
|
|
|
function sizeVerticalScrollbar()
|
|
{
|
|
verticalTrack.height(verticalTrackHeight + 'px');
|
|
verticalDragPosition = 0;
|
|
scrollbarWidth = settings.verticalGutter + verticalTrack.outerWidth();
|
|
|
|
// Make the pane thinner to allow for the vertical scrollbar
|
|
pane.width(paneWidth - scrollbarWidth - originalPaddingTotalWidth);
|
|
|
|
// Add margin to the left of the pane if scrollbars are on that side (to position
|
|
// the scrollbar on the left or right set it's left or right property in CSS)
|
|
if (verticalBar.position().left == 0) {
|
|
pane.css('margin-left', scrollbarWidth + 'px');
|
|
}
|
|
}
|
|
|
|
function initialiseHorizontalScroll()
|
|
{
|
|
if (isScrollableH) {
|
|
|
|
container.append(
|
|
$('<div class="jspHorizontalBar" />').append(
|
|
$('<div class="jspCap jspCapLeft" />'),
|
|
$('<div class="jspTrack" />').append(
|
|
$('<div class="jspDrag" />').append(
|
|
$('<div class="jspDragLeft" />'),
|
|
$('<div class="jspDragRight" />')
|
|
)
|
|
),
|
|
$('<div class="jspCap jspCapRight" />')
|
|
)
|
|
);
|
|
|
|
horizontalBar = container.find('>.jspHorizontalBar');
|
|
horizontalTrack = horizontalBar.find('>.jspTrack');
|
|
horizontalDrag = horizontalTrack.find('>.jspDrag');
|
|
|
|
if (settings.showArrows) {
|
|
arrowLeft = $('<a class="jspArrow jspArrowLeft" />').bind(
|
|
'mousedown.jsp', getArrowScroll(-1, 0)
|
|
).bind('click.jsp', nil);
|
|
arrowRight = $('<a class="jspArrow jspArrowRight" />').bind(
|
|
'mousedown.jsp', getArrowScroll(1, 0)
|
|
).bind('click.jsp', nil);
|
|
if (settings.arrowScrollOnHover) {
|
|
arrowLeft.bind('mouseover.jsp', getArrowScroll(-1, 0, arrowLeft));
|
|
arrowRight.bind('mouseover.jsp', getArrowScroll(1, 0, arrowRight));
|
|
}
|
|
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', function() { return false; });
|
|
|
|
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();
|
|
updateHorizontalArrows();
|
|
} else {
|
|
// no horizontal scroll
|
|
}
|
|
}
|
|
|
|
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 = 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;
|
|
}
|
|
if (isScrollableV) {
|
|
verticalDragHeight = 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;
|
|
}
|
|
}
|
|
|
|
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, doScroll = function()
|
|
{
|
|
if (dirX != 0) {
|
|
positionDragX(horizontalDragPosition + dirX * settings.arrowButtonSpeed, false);
|
|
}
|
|
if (dirY != 0) {
|
|
positionDragY(verticalDragPosition + dirY * settings.arrowButtonSpeed, false);
|
|
}
|
|
},
|
|
scrollInt = setInterval(doScroll, settings.arrowRepeatFreq);
|
|
|
|
doScroll();
|
|
|
|
eve = ele == undefined ? 'mouseup.jsp' : 'mouseout.jsp';
|
|
ele = ele || $('html');
|
|
ele.bind(
|
|
eve,
|
|
function()
|
|
{
|
|
arrow.removeClass('jspActive');
|
|
clearInterval(scrollInt);
|
|
ele.unbind(eve);
|
|
}
|
|
);
|
|
}
|
|
|
|
function cancelDrag()
|
|
{
|
|
$('html').unbind('dragstart.jsp selectstart.jsp mousemove.jsp mouseup.jsp mouseleave.jsp');
|
|
|
|
verticalDrag && verticalDrag.removeClass('jspActive');
|
|
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]);
|
|
}
|
|
|
|
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 == dragMaxY,
|
|
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]);
|
|
}
|
|
|
|
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, eleTop = 0, viewportTop, maxVisibleEleTop, destY;
|
|
|
|
// 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();
|
|
|
|
container.scrollTop(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;
|
|
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);
|
|
}
|
|
// TODO: Implement automatic horizontal scrolling?
|
|
}
|
|
|
|
function contentPositionX()
|
|
{
|
|
return -pane.position().left;
|
|
}
|
|
|
|
function contentPositionY()
|
|
{
|
|
return -pane.position().top;
|
|
}
|
|
|
|
function initMousewheel()
|
|
{
|
|
container.unbind(mwEvent).bind(
|
|
mwEvent,
|
|
function (event, delta, deltaX, deltaY) {
|
|
var dX = horizontalDragPosition, dY = verticalDragPosition;
|
|
positionDragX(horizontalDragPosition + deltaX * settings.mouseWheelSpeed, false)
|
|
positionDragY(verticalDragPosition - 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.unbind('focusin.jsp').bind(
|
|
'focusin.jsp',
|
|
function(e)
|
|
{
|
|
if(e.target === pane[0]){return;}
|
|
scrollToElement(e.target, false);
|
|
}
|
|
);
|
|
}
|
|
|
|
function removeFocusHandler()
|
|
{
|
|
|
|
pane.unbind('focusin.jsp');
|
|
}
|
|
|
|
function initKeyboardNav()
|
|
{
|
|
var pressed, pressedTimer;
|
|
elem.attr('tabindex', 0)
|
|
.unbind('keydown.jsp')
|
|
.bind(
|
|
'keydown.jsp',
|
|
function(e)
|
|
{
|
|
if(e.target !== elem[0]){
|
|
return;
|
|
}
|
|
var dX = horizontalDragPosition, dY = verticalDragPosition, step = pressed ? 2 : 16;
|
|
switch(e.keyCode) {
|
|
case 40: // down
|
|
positionDragY(verticalDragPosition + step, false);
|
|
break;
|
|
case 38: // up
|
|
positionDragY(verticalDragPosition - step, false);
|
|
break;
|
|
case 34: // page down
|
|
case 32: // space
|
|
scrollToY(contentPositionY() + Math.max(32, paneHeight) - 16);
|
|
break;
|
|
case 33: // page up
|
|
scrollToY(contentPositionY() - paneHeight + 16);
|
|
break;
|
|
case 35: // end
|
|
scrollToY(contentHeight - paneHeight);
|
|
break;
|
|
case 36: // home
|
|
scrollToY(0);
|
|
break;
|
|
case 39: // right
|
|
positionDragX(horizontalDragPosition + step, false);
|
|
break;
|
|
case 37: // left
|
|
positionDragX(horizontalDragPosition - step, false);
|
|
break;
|
|
}
|
|
|
|
if( !(dX == horizontalDragPosition && dY == verticalDragPosition) ){
|
|
pressed = true;
|
|
clearTimeout(pressedTimer);
|
|
pressedTimer = setTimeout(function(){
|
|
pressed = false;
|
|
}, 260);
|
|
return false;
|
|
}
|
|
}
|
|
);
|
|
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 removeKeyboardNav()
|
|
{
|
|
elem.attr('tabindex', '-1')
|
|
.removeAttr('tabindex')
|
|
.unbind('keydown.jsp');
|
|
}
|
|
|
|
function observeHash()
|
|
{
|
|
if (location.hash && location.hash.length > 1) {
|
|
var e, retryInt;
|
|
try {
|
|
e = $(location.hash);
|
|
} catch (err) {
|
|
return;
|
|
}
|
|
|
|
if (e.length && pane.find(e)) {
|
|
// nasty workaround but it appears to take a little while before the hash has done its thing
|
|
// to the rendered page so we just wait until the container's scrollTop has been messed up.
|
|
if (container.scrollTop() == 0) {
|
|
retryInt = setInterval(
|
|
function()
|
|
{
|
|
if (container.scrollTop() > 0) {
|
|
scrollToElement(location.hash, true);
|
|
$(document).scrollTop(container.position().top);
|
|
clearInterval(retryInt);
|
|
}
|
|
},
|
|
50
|
|
)
|
|
} else {
|
|
scrollToElement(location.hash, true);
|
|
$(document).scrollTop(container.position().top);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// 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({}, s, settings);
|
|
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 by the specified amount of pixels. animate is optional and if not passed then
|
|
// the value of animateScroll from the settings object this jScrollPane was initialised with is used.
|
|
scrollBy: function(deltaX, deltaY, animate)
|
|
{
|
|
jsp.scrollByX(deltaX, animate);
|
|
jsp.scrollByY(deltaY, animate);
|
|
},
|
|
// Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then
|
|
// the value of animateScroll from the settings object this jScrollPane was initialised with is used.
|
|
scrollByX: function(deltaX, animate)
|
|
{
|
|
var destX = contentPositionX() + deltaX,
|
|
percentScrolled = destX / (contentWidth - paneWidth);
|
|
positionDragX(percentScrolled * dragMaxX, animate);
|
|
},
|
|
// Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then
|
|
// the value of animateScroll from the settings object this jScrollPane was initialised with is used.
|
|
scrollByY: function(deltaY, animate)
|
|
{
|
|
var destY = contentPositionY() + deltaY,
|
|
percentScrolled = destY / (contentHeight - paneHeight);
|
|
positionDragY(percentScrolled * dragMaxY, animate);
|
|
},
|
|
// This method is called when jScrollPane is trying to animate to a new position. You can override
|
|
// it if you want to provide advanced animation functionality. It is passed the following arguments:
|
|
// * ele - the element whose position is being animated
|
|
// * prop - the property that is being animated
|
|
// * value - the value it's being animated to
|
|
// * stepCallback - a function that you must execute each time you update the value of the property
|
|
// You can use the default implementation (below) as a starting point for your own implementation.
|
|
animate: function(ele, prop, value, stepCallback)
|
|
{
|
|
var params = {};
|
|
params[prop] = value;
|
|
ele.animate(
|
|
params,
|
|
{
|
|
'duration' : settings.animateDuration,
|
|
'ease' : settings.animateEase,
|
|
'queue' : false,
|
|
'step' : stepCallback
|
|
}
|
|
);
|
|
},
|
|
// Returns the current x position of the viewport with regards to the content pane.
|
|
getContentPositionX: function()
|
|
{
|
|
return contentPositionX();
|
|
},
|
|
// Returns the current y position of the viewport with regards to the content pane.
|
|
getContentPositionY: function()
|
|
{
|
|
return contentPositionY();
|
|
},
|
|
// 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();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// Pluginifying code...
|
|
|
|
settings = $.extend({}, $.fn.jScrollPane.defaults, settings);
|
|
|
|
var ret;
|
|
this.each(
|
|
function()
|
|
{
|
|
var elem = $(this), jspApi = elem.data('jsp');
|
|
if (jspApi) {
|
|
jspApi.reinitialise(settings);
|
|
} else {
|
|
jspApi = new JScrollPane(elem, settings);
|
|
elem.data('jsp', jspApi);
|
|
}
|
|
ret = ret ? ret.add(elem) : elem;
|
|
}
|
|
)
|
|
return ret;
|
|
};
|
|
|
|
$.fn.jScrollPane.defaults = {
|
|
'showArrows' : false,
|
|
'maintainPosition' : true,
|
|
'autoReinitialise' : false,
|
|
'autoReinitialiseDelay' : 500,
|
|
'verticalDragMinHeight' : 0,
|
|
'verticalDragMaxHeight' : 99999,
|
|
'horizontalDragMinWidth' : 0,
|
|
'horizontalDragMaxWidth' : 99999,
|
|
'animateScroll' : false,
|
|
'animateDuration' : 300,
|
|
'animateEase' : 'linear',
|
|
'hijackInternalLinks' : false,
|
|
'verticalGutter' : 4,
|
|
'horizontalGutter' : 4,
|
|
'mouseWheelSpeed' : 10,
|
|
'arrowButtonSpeed' : 10,
|
|
'arrowRepeatFreq' : 100,
|
|
'arrowScrollOnHover' : false,
|
|
'verticalArrowPositions' : 'split',
|
|
'horizontalArrowPositions' : 'split',
|
|
'enableKeyboardNavigation' : true,
|
|
'hideFocus' : false
|
|
};
|
|
|
|
})(jQuery,this);
|
|
|