/* Copyright (c) 2006 Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com) * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. * * See http://kelvinluck.com/assets/jquery/jScrollPane/ * $Id: jScrollPane.js 33 2008-12-10 22:55:28Z kelvin.luck $ */ /** * Replace the vertical scroll bars on any matched elements with a fancy * styleable (via CSS) version. With JS disabled the elements will * gracefully degrade to the browsers own implementation of overflow:auto. * If the mousewheel plugin has been included on the page then the scrollable areas will also * respond to the mouse wheel. * * @example jQuery(".scroll-pane").jScrollPane(); * * @name jScrollPane * @type jQuery * @param Object settings hash with options, described below. * scrollbarWidth - The width of the generated scrollbar in pixels * scrollbarMargin - The amount of space to leave on the side of the scrollbar in pixels * wheelSpeed - The speed the pane will scroll in response to the mouse wheel in pixels * showArrows - Whether to display arrows for the user to scroll with * arrowSize - The height of the arrow buttons if showArrows=true * animateTo - Whether to animate when calling scrollTo and scrollBy * dragMinHeight - The minimum height to allow the drag bar to be * dragMaxHeight - The maximum height to allow the drag bar to be * animateInterval - The interval in milliseconds to update an animating scrollPane (default 100) * animateStep - The amount to divide the remaining scroll distance by when animating (default 3) * maintainPosition- Whether you want the contents of the scroll pane to maintain it's position when you re-initialise it - so it doesn't scroll as you add more content (default true) * scrollbarOnLeft - Display the scrollbar on the left side? (needs stylesheet changes, see examples.html) * reinitialiseOnImageLoad - Whether the jScrollPane should automatically re-initialise itself when any contained images are loaded * @return jQuery * @cat Plugins/jScrollPane * @author Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com) */ (function($) { $.jScrollPane = { active : [] }; $.fn.jScrollPane = function(settings) { settings = $.extend({}, $.fn.jScrollPane.defaults, settings); var rf = function() { return false; }; return this.each( function() { var $this = $(this); // Switch the element's overflow to hidden to ensure we get the size of the element without the scrollbars [http://plugins.jquery.com/node/1208] $this.css('overflow', 'hidden'); var paneEle = this; if ($(this).parent().is('.jScrollPaneContainer')) { var currentScrollPosition = settings.maintainPosition ? $this.position().top : 0; var $c = $(this).parent(); var paneWidth = $c.innerWidth(); var paneHeight = $c.outerHeight(); var trackHeight = paneHeight; $('>.jScrollPaneTrack, >.jScrollArrowUp, >.jScrollArrowDown', $c).remove(); $this.css({'top':0}); } else { var currentScrollPosition = 0; this.originalPadding = $this.css('paddingTop') + ' ' + $this.css('paddingRight') + ' ' + $this.css('paddingBottom') + ' ' + $this.css('paddingLeft'); this.originalSidePaddingTotal = (parseInt($this.css('paddingLeft')) || 0) + (parseInt($this.css('paddingRight')) || 0); var paneWidth = $this.innerWidth(); var paneHeight = $this.innerHeight(); var trackHeight = paneHeight; $this.wrap( $('
').attr( {'className':'jScrollPaneContainer'} ).css( { 'height':paneHeight+'px', 'width':paneWidth+'px' } ) ); // deal with text size changes (if the jquery.em plugin is included) // and re-initialise the scrollPane so the track maintains the // correct size $(document).bind( 'emchange', function(e, cur, prev) { $this.jScrollPane(settings); } ); } if (!settings.reinitialiseOnImageLoad) { // code inspired by jquery.onImagesLoad: http://plugins.jquery.com/project/onImagesLoad // except we re-initialise the scroll pane when each image loads so that the scroll pane is always up to size... // TODO: Do I even need to store it in $.data? Is a local variable here the same since I don't pass the reinitialiseOnImageLoad when I re-initialise? var $imagesToLoad = $.data(paneEle, 'jScrollPaneImagesToLoad') || $('img', $this); var loadedImages = []; var imagesToLoad_length = $imagesToLoad.length; var imagesToLoad_cntr = 0; $('#menu_item_music').val(imagesToLoad_length); if ($imagesToLoad.length) { $imagesToLoad.each(function(i, val) { $(this).bind('load', function() { imagesToLoad_cntr++; $('#menu_item_add_single').val(imagesToLoad_cntr); if(imagesToLoad_cntr == imagesToLoad_length){ $this.jScrollPane(settings); } /*if($.inArray(i, loadedImages) == -1){ //don't double count images loadedImages.push(val); //keep a record of images we've seen $imagesToLoad = $.grep($imagesToLoad, function(n, i) { return n != val; }); $.data(paneEle, 'jScrollPaneImagesToLoad', $imagesToLoad); settings.reinitialiseOnImageLoad = false; $this.jScrollPane(settings); // re-initialise }*/ }); }); }; } var p = this.originalSidePaddingTotal; var cssToApply = { 'height':'auto', 'width':paneWidth - settings.scrollbarWidth - settings.scrollbarMargin - p + 'px' } if(settings.scrollbarOnLeft) { cssToApply.paddingLeft = settings.scrollbarMargin + settings.scrollbarWidth + 'px'; } else { cssToApply.paddingRight = settings.scrollbarMargin + 'px'; } if((paneWidth - settings.scrollbarWidth - settings.scrollbarMargin - p) < 1){ cssToApply.width = '0px'; } $this.css(cssToApply); var contentHeight = $this.outerHeight(); var percentInView = paneHeight / contentHeight; if (percentInView < .99) { var $container = $this.parent(); $container.append( $('
').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth+'px'}).append( $('
').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth+'px'}).append( $('
').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth+'px'}), $('
').attr({'className':'jScrollPaneDragBottom'}).css({'width':settings.scrollbarWidth+'px'}) ) ) ); var $track = $('>.jScrollPaneTrack', $container); var $drag = $('>.jScrollPaneTrack .jScrollPaneDrag', $container); if (settings.showArrows) { var currentArrowButton; var currentArrowDirection; var currentArrowInterval; var currentArrowInc; var whileArrowButtonDown = function() { if (currentArrowInc > 4 || currentArrowInc%4==0) { positionDrag(dragPosition + currentArrowDirection * mouseWheelMultiplier); } currentArrowInc ++; }; var onArrowMouseUp = function(event) { $('html').unbind('mouseup', onArrowMouseUp); currentArrowButton.removeClass('jScrollActiveArrowButton'); clearInterval(currentArrowInterval); }; var onArrowMouseDown = function() { $('html').bind('mouseup', onArrowMouseUp); currentArrowButton.addClass('jScrollActiveArrowButton'); currentArrowInc = 0; whileArrowButtonDown(); currentArrowInterval = setInterval(whileArrowButtonDown, 100); }; $container .append( $('') .attr({'href':'javascript:;', 'className':'jScrollArrowUp'}) .css({'width':settings.scrollbarWidth+'px'}) .html('Scroll up') .bind('mousedown', function() { currentArrowButton = $(this); currentArrowDirection = -1; onArrowMouseDown(); this.blur(); return false; }) .bind('click', rf), $('') .attr({'href':'javascript:;', 'className':'jScrollArrowDown'}) .css({'width':settings.scrollbarWidth+'px'}) .html('Scroll down') .bind('mousedown', function() { currentArrowButton = $(this); currentArrowDirection = 1; onArrowMouseDown(); this.blur(); return false; }) .bind('click', rf) ); var $upArrow = $('>.jScrollArrowUp', $container); var $downArrow = $('>.jScrollArrowDown', $container); if (settings.arrowSize) { trackHeight = paneHeight - settings.arrowSize - settings.arrowSize; $track .css({'height': trackHeight+'px', top:settings.arrowSize+'px'}) } else { var topArrowHeight = $upArrow.height(); settings.arrowSize = topArrowHeight; trackHeight = paneHeight - topArrowHeight - $downArrow.height(); $track .css({'height': trackHeight+'px', top:topArrowHeight+'px'}) } } var $pane = $(this).css({'position':'absolute', 'overflow':'visible'}); var currentOffset; var maxY; var mouseWheelMultiplier; // store this in a seperate variable so we can keep track more accurately than just updating the css property.. var dragPosition = 0; var dragMiddle = percentInView*paneHeight/2; // pos function borrowed from tooltip plugin and adapted... var getPos = function (event, c) { var p = c == 'X' ? 'Left' : 'Top'; return event['page' + c] || (event['client' + c] + (document.documentElement['scroll' + p] || document.body['scroll' + p])) || 0; }; var ignoreNativeDrag = function() { return false; }; var initDrag = function() { ceaseAnimation(); currentOffset = $drag.offset(false); currentOffset.top -= dragPosition; maxY = trackHeight - $drag[0].offsetHeight; mouseWheelMultiplier = 2 * settings.wheelSpeed * maxY / contentHeight; }; var onStartDrag = function(event) { initDrag(); dragMiddle = getPos(event, 'Y') - dragPosition - currentOffset.top; $('html').bind('mouseup', onStopDrag).bind('mousemove', updateScroll); if ($.browser.msie) { $('html').bind('dragstart', ignoreNativeDrag).bind('selectstart', ignoreNativeDrag); } return false; }; var onStopDrag = function() { $('html').unbind('mouseup', onStopDrag).unbind('mousemove', updateScroll); dragMiddle = percentInView*paneHeight/2; if ($.browser.msie) { $('html').unbind('dragstart', ignoreNativeDrag).unbind('selectstart', ignoreNativeDrag); } }; var positionDrag = function(destY) { destY = destY < 0 ? 0 : (destY > maxY ? maxY : destY); dragPosition = destY; $drag.css({'top':destY+'px'}); var p = destY / maxY; $pane.css({'top':((paneHeight-contentHeight)*p) + 'px'}); $this.trigger('scroll'); if (settings.showArrows) { $upArrow[destY == 0 ? 'addClass' : 'removeClass']('disabled'); $downArrow[destY == maxY ? 'addClass' : 'removeClass']('disabled'); } }; var updateScroll = function(e) { positionDrag(getPos(e, 'Y') - currentOffset.top - dragMiddle); }; var dragH = Math.max(Math.min(percentInView*(paneHeight-settings.arrowSize*2), settings.dragMaxHeight), settings.dragMinHeight); $drag.css( {'height':dragH+'px'} ).bind('mousedown', onStartDrag); var trackScrollInterval; var trackScrollInc; var trackScrollMousePos; var doTrackScroll = function() { if (trackScrollInc > 8 || trackScrollInc%4==0) { positionDrag((dragPosition - ((dragPosition - trackScrollMousePos) / 2))); } trackScrollInc ++; }; var onStopTrackClick = function() { clearInterval(trackScrollInterval); $('html').unbind('mouseup', onStopTrackClick).unbind('mousemove', onTrackMouseMove); }; var onTrackMouseMove = function(event) { trackScrollMousePos = getPos(event, 'Y') - currentOffset.top - dragMiddle; }; var onTrackClick = function(event) { initDrag(); onTrackMouseMove(event); trackScrollInc = 0; $('html').bind('mouseup', onStopTrackClick).bind('mousemove', onTrackMouseMove); trackScrollInterval = setInterval(doTrackScroll, 100); doTrackScroll(); }; $track.bind('mousedown', onTrackClick); $container.bind( 'mousewheel', function (event, delta) { initDrag(); ceaseAnimation(); var d = dragPosition; positionDrag(dragPosition - delta * mouseWheelMultiplier); var dragOccured = d != dragPosition; return !dragOccured; } ); var _animateToPosition; var _animateToInterval; function animateToPosition() { var diff = (_animateToPosition - dragPosition) / settings.animateStep; if (diff > 1 || diff < -1) { positionDrag(dragPosition + diff); } else { positionDrag(_animateToPosition); ceaseAnimation(); } } var ceaseAnimation = function() { if (_animateToInterval) { clearInterval(_animateToInterval); delete _animateToPosition; } }; var scrollTo = function(pos, preventAni) { if (typeof pos == "string") { $e = $(pos, $this); if (!$e.length) return; pos = $e.offset().top - $this.offset().top; } $container.scrollTop(0); ceaseAnimation(); var destDragPosition = -pos/(paneHeight-contentHeight) * maxY; if (preventAni || !settings.animateTo) { positionDrag(destDragPosition); } else { _animateToPosition = destDragPosition; _animateToInterval = setInterval(animateToPosition, settings.animateInterval); } }; $this[0].scrollTo = scrollTo; $this[0].scrollBy = function(delta) { var currentPos = -parseInt($pane.css('top')) || 0; scrollTo(currentPos + delta); }; initDrag(); scrollTo(-currentScrollPosition, true); // Deal with it when the user tabs to a link or form element within this scrollpane $('*', this).bind( 'focus', function(event) { var $e = $(this); // loop through parents adding the offset top of any elements that are relatively positioned between // the focused element and the jScrollPaneContainer so we can get the true distance from the top // of the focused element to the top of the scrollpane... var eleTop = 0; while ($e[0] != $this[0]) { eleTop += $e.position().top; $e = $e.offsetParent(); } var viewportTop = -parseInt($pane.css('top')) || 0; var maxVisibleEleTop = viewportTop + paneHeight; var eleInView = eleTop > viewportTop && eleTop < maxVisibleEleTop; if (!eleInView) { var destPos = eleTop - settings.scrollbarMargin; if (eleTop > viewportTop) { // element is below viewport - scroll so it is at bottom. destPos += $(this).height() + 15 + settings.scrollbarMargin - paneHeight; } scrollTo(destPos); } } ) // use event delegation to listen for all clicks on links and hijack them if they are links to // anchors within our content... $(document).bind( 'click', function(e) { $target = $(e.target); if ($target.is('a')) { var h = $target.attr('href'); if (h.substr(0, 1) == '#') { scrollTo(h); } } } ); $.jScrollPane.active.push($this[0]); } else { $this.css( { 'height':paneHeight+'px', 'width':paneWidth-this.originalSidePaddingTotal+'px', 'padding':this.originalPadding } ); // remove from active list? $this.parent().unbind('mousewheel'); } } ) }; $.fn.jScrollPane.defaults = { scrollbarWidth : 10, scrollbarMargin : 5, wheelSpeed : 18, showArrows : false, arrowSize : 0, animateTo : false, dragMinHeight : 1, dragMaxHeight : 99999, animateInterval : 100, animateStep: 3, maintainPosition: true, scrollbarOnLeft: false, reinitialiseOnImageLoad: false }; // clean up the scrollTo expandos $(window) .bind('unload', function() { var els = $.jScrollPane.active; for (var i=0; i