/* 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;
   this.originalSidePaddingTotal = 0;
   
   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(
     $('<div></div>').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 = [];
    
    if ($imagesToLoad.length) {
     $imagesToLoad.each(function(i, val) {
      $(this).bind('load', function() {
       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
       }
      }).each(function(i, val) {
       if(this.complete || this.complete===undefined) { 
        //needed for potential cached images
        this.src = this.src; 
       } 
      });
     });
    };
   }

   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';
   }
   
   // problem
   $this.css(cssToApply);
   

   var contentHeight = $this.outerHeight();
   var percentInView = paneHeight / contentHeight;

   if (percentInView < .99) {
    var $container = $this.parent();
    $container.append(
     $('<div></div>').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth+'px'}).append(
      $('<div></div>').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth+'px'}).append(
       $('<div></div>').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth+'px'}),
       $('<div></div>').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()
     {
      //alert (currentArrowInc);
      if (currentArrowInc > 4 || currentArrowInc%4==0) {
       // alert ("positionDrag: " + dragPosition + "\ncurrentArrowDirection: " + currentArrowDirection + "\nmouseWheelMultiplier: " + mouseWheelMultiplier);
       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(
       $('<a></a>')
        .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),
       $('<a></a>')
        .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);
      }
     }
    )
    
    
    if (location.hash) {
     scrollTo(location.hash);
    }
    
    // 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 : 11,
scrollbarMargin : 5,
wheelSpeed : 10,
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<els.length; i++) {
   els[i].scrollTo = els[i].scrollBy = null;
  }
}
);

})(jQuery);
