/* 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 19 2008-11-13 06:00:09Z 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)
 */
jQuery.jScrollPane = {
  active : []
};
jQuery.fn.jScrollPane = function(settings)
{
  settings = jQuery.extend({}, jQuery.fn.jScrollPane.defaults, settings);
  var rf = function() {
    return false;
  };
  return this.each(
      function()
      {
        var $this = jQuery(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 (jQuery(this).parent().is('.jScrollPaneContainer')) {
          var currentScrollPosition = settings.maintainPosition ? $this.offset({relativeTo:jQuery(this).parent()[0]}).top : 0;
          var $c = jQuery(this).parent();
          var paneWidth = $c.innerWidth();
          var paneHeight = $c.outerHeight();
          var trackHeight = paneHeight;
          jQuery('>.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(
              jQuery('<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
          jQuery(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 (jQuery.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';
        }
        $this.css(cssToApply);
        var contentHeight = $this.outerHeight();
        var percentInView = paneHeight / contentHeight;
        if (percentInView < .99) {
          var $container = $this.parent();
          $container.append(
              jQuery('<div></div>').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth + 'px'}).append(
                  jQuery('<div></div>').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth + 'px'}).append(
                      jQuery('<div></div>').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth + 'px'}),
                      jQuery('<div></div>').attr({'className':'jScrollPaneDragBottom'}).css({'width':settings.scrollbarWidth + 'px'})
                      )
                  )
              );
          var $track = jQuery('>.jScrollPaneTrack', $container);
          var $drag = jQuery('>.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)
            {
              jQuery('html').unbind('mouseup', onArrowMouseUp);
              currentArrowButton.removeClass('jScrollActiveArrowButton');
              clearInterval(currentArrowInterval);
              //console.log($(event.target));
              //currentArrowButton.parent().removeClass('jScrollArrowUpClicked jScrollArrowDownClicked');
            };
            var onArrowMouseDown = function() {
              //console.log(direction);
              //currentArrowButton = $(this);
              jQuery('html').bind('mouseup', onArrowMouseUp);
              currentArrowButton.addClass('jScrollActiveArrowButton');
              currentArrowInc = 0;
              whileArrowButtonDown();
              currentArrowInterval = setInterval(whileArrowButtonDown, 100);
            };
            $container
                .append(
                jQuery('<a></a>')
                    .attr({'href':'javascript:;', 'className':'jScrollArrowUp'})
                    .css({'width':settings.scrollbarWidth + 'px'})
                    .html('Scroll up')
                    .bind('mousedown', function()
                {
                  currentArrowButton = jQuery(this);
                  currentArrowDirection = -1;
                  onArrowMouseDown();
                  this.blur();
                  return false;
                })
                    .bind('click', rf),
                jQuery('<a></a>')
                    .attr({'href':'javascript:;', 'className':'jScrollArrowDown'})
                    .css({'width':settings.scrollbarWidth + 'px'})
                    .html('Scroll down')
                    .bind('mousedown', function()
                {
                  currentArrowButton = jQuery(this);
                  currentArrowDirection = 1;
                  onArrowMouseDown();
                  this.blur();
                  return false;
                })
                    .bind('click', rf)
                );
            var $upArrow = jQuery('>.jScrollArrowUp', $container);
            var $downArrow = jQuery('>.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 = jQuery(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;
            jQuery('html').bind('mouseup', onStopDrag).bind('mousemove', updateScroll);
            if (jQuery.browser.msie) {
              jQuery('html').bind('dragstart', ignoreNativeDrag).bind('selectstart', ignoreNativeDrag);
            }
            return false;
          };
          var onStopDrag = function()
          {
            jQuery('html').unbind('mouseup', onStopDrag).unbind('mousemove', updateScroll);
            dragMiddle = percentInView * paneHeight / 2;
            if (jQuery.browser.msie) {
              jQuery('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);
            jQuery('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;
            jQuery('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 = jQuery(pos, this);
              if (!$e.length) return;
              pos = $e.offset().top - $this.offset().top;
            }
            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 eleTop = $(this).position().top;
                var viewportTop = -parseInt($pane.css('top')) || 0;
                var maxVisibleEleTop = viewportTop + paneHeight;
                var eleInView = eleTop > viewportTop && eleTop < maxVisibleEleTop;
                if (!eleInView) {
                  $container.scrollTop(0);
                  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) {
            // the timeout needs to be longer in IE when not loading from cache...
            setTimeout(function() {
              $(location.hash, $this).trigger('focus');
            }, $.browser.msie ? 100 : 0);
          }
          // 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');
                  //console.log(h);
                  if (h.substr(0, 1) == '#') {
                    $linkedEle = $(h, $this);
                    //console.log($linkedEle);
                    if ($linkedEle.length) {
                      $linkedEle.trigger('focus');
                      return false;
                    }
                  }
                }
              }
              );
          jQuery.jScrollPane.active.push($this[0]);
        } else {
          $this.css(
          {
            'height':paneHeight + 'px',
            'width':paneWidth - this.originalSidePaddingTotal + 'px',
            'padding':this.originalPadding
          }
              );
          // remove from active list?
        }
      }
      )
};
jQuery.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
jQuery(window)
    .bind('unload', function() {
  var els = jQuery.jScrollPane.active;
  for (var i = 0; i < els.length; i++) {
    els[i].scrollTo = els[i].scrollBy = null;
  }
}
    );