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