// vertically scroll the elements in a container. Assumes the original item has a fixed height and 
// overflow: scroll and changes
// it to overflow: hidden but the elements move.
// Stops scrolling on hover.
// puts the various functions into the container elements so they can be used publicly
// as in $('.x').scroller(); $('.x')[0].scroller.stop(); stops the first scroller
// Options:
// speed: {Number} ms between one-pixel scrolls. Smaller number is faster. Default: 100
// find: {String|Element|jQuery object} The items to scroll. Default: all the elements in the item
// hover: {Boolean} True to stop scrolling on hover, and display arrows to manually move. Default: true
// expand: {Boolean} True to allow expanding the list over the page contents. Default: true
// Dependencies: jquery.yi.js jquery.effects.js
(function($){
$.fn.scroller = $.extend(function (opts){
  opts = $.extend({}, arguments.callee['default'], opts);
  return this.each(function(){
    var $this = $(this);
    var find = opts.find == 'all' ? $this.textChildren() : opts.find;
    this.scroller = new $.fn.scroller.Scroller ($this, $(find,this), opts.speed, opts.hover, opts.expand);
  });
},{ // option sets
  'default': {speed: 100, find: 'all', hover: true, expand: true},
  fast: {speed: 10},
  nohover: {hover: false, expand: false}
});

// We need to pass the elements (a jQuery of elements to scroll) because there is no way for the program to know
// the desired granularity of the scrolling (trying to scroll a whole <ul> would flash or
// leave large blank spots as the top scrolled off, so we need to pass an array of <li>'s)
$.fn.scroller.Scroller = function (container, elements, speed, doHover, doExpand){

  // public members
  this.speed = speed; // ms between 1-pixel scrolls

  this.scrollN = function(n) {
    elements.each (function(){
      var top = parseInt(this.style.top)-n;
      if (top < -this.xbottom){ // scrolled off the top
        top = lowestTop-this.xbottom;
      }else if (top > lowestTop - this.xbottom){ // scrolled off the bottom
        top = -this.xbottom;
      } // if
      this.style.top = top+'px';
    }); // each
  }; // scrollN

  this.scroll = function() {
    if (timer != null){
      expandButton.hide();
      up.hide();
      dn.hide();
      me.scrollN(1);
    }
    timer = setTimeout (me.scroll, me.speed);
  };  // scroll

  this.stop = function() {
    if (doExpand) expandButton.show();
    if (doHover){
      up.show();
      dn.show();
    }
    clearTimeout(timer);
    timer = null;
  }; // stop

  this.reset = function(){
    elements.each(function(){
      this.style.top = '0px';
    });
  }; // reset 

  this.expand = function(){
    container.unbind('hover').css({
      height: 'auto'
    }).cheapDropShadow({style: {position: 'absolute'}});
    expandButton.hide();
    up.hide();
    dn.hide();
    contractButton.show();
    me.reset();
  }; // expand

  this.contract = function(){
    container.hover(me.stop, me.scroll).css({
      height: container.oldHeight
    }).undoDropShadow();
    expandButton.show();
    up.show();
    dn.show();
    contractButton.hide();
  }; // contract

  // browser-independent way to find the absolute position of an element
  // from http://www.quirksmode.org/js/findpos.html
  function top (e){
    for (var result = 0; e.offsetParent; e=e.offsetParent) result += e.offsetTop;
    return result;
  } // top
  function left (e){
    for (var result = 0; e.offsetParent; e=e.offsetParent) result += e.offsetLeft;
    return result;
  } // left
  
  // constructor
  var me = this; // save our context so we can call it from setTimeout
  container.hover(this.stop, this.scroll).css({overflow: 'hidden', position: 'relative'}); // need to give it position so I can place images relative to it
  container.oldHeight = container[0].offsetHeight;
  // create the scrolling buttons
  var expandHeight; // how tall is the expand button, so I know where to place the up button
  var expandButton = $.imgButton('squelettes/javascript/maximize.gif','squelettes/javascript/maximize_over.gif','squelettes/javascript/maximize_over_down.gif').
    appendTo (container).css({ position: 'absolute', top: '0px', right: '0px'}).
    each(function(){ expandHeight = (doExpand ? this.offsetHeight : 0) + 'px' }).
    hide().
    click(me.expand);
 var contractButton = $.imgButton('squelettes/javascript/restore.gif','squelettes/javascript/restore_over.gif','squelettes/javascript/restore_over_down.gif').
    css({ position: 'absolute', top: '0px', right: '0px'}).hide().
    click(me.contract).
    appendTo (container);
  // hack to correct for IE box model problem
  if ($.browser.msie) contractButton.css({ top: '1px', right: '11px'});
  // note hard-wired top (height of expand image)
  var up = $.IMG ({className: 'fixPNG', src: 'squelettes/javascript/arrow_up blue.png', style: {position: 'absolute', top: expandHeight, right: '0px'}}).hide().
    mousedown (function() { me.clicktimer = setInterval (function() {me.scrollN(-1);}, me.speed/10); return false;}).
    mouseup (f = function() { clearInterval(me.clicktimer); return false; }).
    mousemove (f).
    appendTo (container);
  var dn = $.IMG ({className: 'fixPNG', src: 'squelettes/javascript/arrow_down blue.png', style: {position: 'absolute', bottom: '0px', right: '0px'}}).hide().
    mousedown (function() { me.clicktimer = setInterval (function() {me.scrollN(1);}, me.speed/10); return false;}).
    mouseup (f = function() { clearInterval(me.clicktimer); return false; }).
    mousemove (f).
    appendTo (container);
  // remove the expand link, since we use the button
  $('.expand').hide();
  // create the scrolling timer
  var timer = null;
  var lowest = 0;
  var lowestTop = 0;
  elements.each (function(i){
    this.xbottom = top(this) + this.offsetHeight - top(container[0]);
    $(this).css({position: 'relative', top: '0px'});
    if (top(this) > lowestTop){
      lowestTop = top(this);
      lowest = i;
    }
  });
  lowestTop -= top(container[0]);
  lowestTop += elements[lowest].offsetHeight;

  timer = setTimeout (me.scroll, me.speed);  // start scrolling

} // Scroller


// returns a set of leaf-like elements (leafy?) that either contain no children, or contain one
// or more non-HTMLElement children. These are the finest-grained elements that can be manipulated 
// together, since HTMLElements are the only nodes that can be manipulated with CSS.
$.fn.textChildren = function(){
  var ret = $([]);
  this.each(function(){
    if (this.nodeType != 1) return; // only look at HTMLElements
    if (/^(li|img|h1|h2|h3|h4|h5|h6)$/i.test(this.tagName)){ // some things we know are leaves
      ret = ret.add(this);
      return;
    }
    if (!this.firstChild){ // no kids
      ret=ret.add(this);
      return;
    }
    for (var node = this.firstChild; node; node = node.nextSibling) if (node.nodeType == 3 && $.trim(node.data)!=''){
      // if this has any text nodes with content, it's leafy
      ret=ret.add(this);
      return;
    }
    ret=ret.add($(this.childNodes).textChildren()); // this is a branch; find the leaves
  });
  return ret;
}; // textChildren

})(jQuery);

