/*
 balloon.js -- a DHTML library for balloon tooltips

 Sheldon McKay <mckays@cshl.edu>
 $Id: balloon.js,v 1.5 2007/04/02 19:16:43 smckay Exp $

 See http://www.wormbase.org/wiki/index.php/Balloon_Tooltips
 for documentation.
*/


// Only two global variables.  These are necessary to avoid losing
// scope when setting the balloon timeout and so that multiple instances
// of the balloon class know if a balloon is currently visible on the
// page.
var currentBalloonClass;
var balloonIsVisible;


// constructor for balloon class
// Each instance of this class will be populated with default configuration
// variables that can be overwritten
var Balloon = function() {

  // Balloon connector (the triangle part) images
  // Default images are provided, they are 48 px in height
  this.upLeftConnector    = '/img/tooltip/balloon_up_bottom_left.png';
  this.upRightConnector   = '/img/tooltip/balloon_up_bottom_right.png';
  this.downLeftConnector  = '/img/tooltip/balloon_down_top_left.png';
  this.downRightConnector = '/img/tooltip/balloon_down_top_right.png';

  // Balloon body background images
  // NOTE: the balloon body height is exaggerated to allow for a sliding
  // non-repeat background image for an arbitrary amount of text. The
  // height of the default balloon images is 900px, but only a small part
  // is usually visible, depending on the content dimensions. 
  this.upBalloon   = '/img/tooltip/balloon_up_top.png';
  this.downBalloon = '/img/tooltip/balloon_down_bottom.png';

  // Balloon dimensions and text placement
  // the default width 300px images (8 pixel shadow)
  this.balloonWidth     = '308px';
  this.paddingTop       = '20px';
  this.paddingLeft      = '15px';
  this.paddingRight     = '15px';
  this.paddingBottom    = '20px';
  this.paddingConnector = '48px';

  // Horizontal offset: allowed values are 'left' and 'right'
  // the offset will be flipped as required to keep the balloon onscreen
  this.hOffset = 'right';

  // Location of optional ajax handler that returns tooltip contents
  this.helpUrl = null;
  //    this.helpUrl = '/db/misc/help';

  // Default tooltip text size
  this.balloonTextSize = '90%';

  // Delay (milliseconds) before balloon is displayed
  // Don't set it too low or you may annoy your users!
  this.delayTime = 750;

  this.vOffset = '5px'
  this.isIE    = document.all && !window.opera;
  this.isOpera = window.opera;
}

/////////////////////////////////////////////////////////////////////////
// This is the function that is called on mouseover.  It has a built-in
// delay time to avoid balloons popping up on rapid mousover events
/////////////////////////////////////////////////////////////////////////

Balloon.prototype.showTooltip = function(evt,caption,sticky) {
  if (balloonIsVisible) return false;
  var el = this.getEventTarget(evt);
  
  // attach a mousout event to the target element
  el.onmouseout = this.hideTooltip;

  // set the active coordinates
  this.setActiveCoordinates(evt);

  // Opera tooltip workaround
  if (this.isOpera && (el.getAttribute('title') || el.getAttribute('href')) ) 
    sticky = true;
 
  this.balloonIsStatic ? this.hideStaticTooltip() : this.hideTooltip();  
  this.balloonIsStatic = sticky;
  this.currentHelpText = caption;

  currentBalloonClass = this;
  this.timeoutTooltip = window.setTimeout(this.doShowTooltip,this.delayTime);
}

/////////////////////////////////////////////////////////////////////
// Tooltip rendering function
/////////////////////////////////////////////////////////////////////

Balloon.prototype.doShowTooltip = function() {
  var bSelf = currentBalloonClass;

  // Stop firing if a balloon is already being displayed
  if (balloonIsVisible) return false;  

  // make sure user-configured numbers are not strings
  bSelf.parseIntAll();

  // actual window dimensions
  var pageWidth  = YAHOO.util.Dom.getViewportWidth();
  var pageHeight = YAHOO.util.Dom.getViewportHeight();
  var pageTop    = bSelf.isIE ? document.body.scrollTop : window.pageYOffset;
  var pageMid    = pageTop + pageHeight/2;
  var pageBottom = pageTop + pageHeight;

  // balloon placement tied to onmouseover element
  var left,hOrient;
  if (bSelf.activeLeft < bSelf.balloonWidth) {
    hOrient = 'right';
    left = bSelf.activeRight;
  }
  else if ((bSelf.activeRight + bSelf.balloonWidth) > pageWidth) {
    hOrient = 'left';
    left = bSelf.activeLeft - bSelf.balloonWidth;
  }
  else {
    hOrient = bSelf.hOffset;
    left = hOrient == 'left' ? (bSelf.activeLeft - bSelf.balloonWidth) : bSelf.activeRight;
  }

  // balloon is up if below midline, down otherwise
  var top,vOrient;
  if (bSelf.activeTop > pageMid) {
    vOrient = 'up';
    top = bSelf.activeTop - Math.abs(bSelf.vOffset);
  }
  else {
    vOrient = 'down';
    top = bSelf.activeBottom + Math.abs(bSelf.vOffset);
  }

  // Get or create the balloon layer.
  bSelf.activeBalloon = bSelf.getElement(vOrient) || bSelf.createAndAppend(vOrient);
  bSelf.setStyle(bSelf.activeBalloon,'display','none');
  bSelf.setStyle(bSelf.activeBalloon,'position','absolute');

  // look for url 
  if (bSelf.currentHelpText.match(/^url:/i)) {
    var urlArray = currentHelpText.split(':');
    bSelf.activeUrl = urlArray[1];
  }

  // request the contents synchronously (ie wait for result)
  var helpText = bSelf.getContents(bSelf.currentHelpText);

  // configure for up or down orientation
  if (vOrient == 'up') {
    var upConnector = hOrient == 'left' ?  bSelf.upLeftConnector : bSelf.upRightConnector; 
    bSelf.setStyle(bSelf.activeBalloon,'background','url('+upConnector+') bottom left no-repeat');
    bSelf.setStyle(bSelf.activeBalloon,'padding-bottom',bSelf.paddingConnector);
    bSelf.setStyle(bSelf.activeBalloon,'padding-top',bSelf.paddingTop);
    var upCaption = bSelf.getElement('upCaption') || bSelf.createAndAppend('upCaption',bSelf.activeBalloon);
    var upText    = bSelf.getElement('upText')    || bSelf.createAndAppend('upText',upCaption);
    bSelf.setStyle(upCaption,'background','url('+bSelf.upBalloon+') top left no-repeat');
    bSelf.setStyle(upCaption,'padding-top',bSelf.paddingTop);    
    bSelf.setStyle(upCaption,'padding-bottom',1);
    bSelf.activeBody = upCaption;
    bSelf.activeText    = upText;
  }
  else {
    var downConnector = hOrient == 'left' ?  bSelf.downLeftConnector : bSelf.downRightConnector;
    bSelf.setStyle(bSelf.activeBalloon,'background','url('+downConnector+') top left no-repeat');
    bSelf.setStyle(bSelf.activeBalloon,'padding-bottom',bSelf.paddingBottom);
    bSelf.setStyle(bSelf.activeBalloon,'padding-top',bSelf.paddingConnector);
    var downCaption = bSelf.getElement('downCaption') || bSelf.createAndAppend('downCaption',bSelf.activeBalloon);
    var downText    = bSelf.getElement('downText')    || bSelf.createAndAppend('downText',downCaption);
    bSelf.setStyle(downCaption,'background','url('+bSelf.downBalloon+') bottom left no-repeat');
    bSelf.setStyle(downCaption,'padding-top',1);
    bSelf.setStyle(downCaption,'padding-bottom',bSelf.paddingBottom);
    bSelf.activeBody = downCaption;
    bSelf.activeText    = downText;
  }
  
  // text boundaries
  bSelf.setStyle(bSelf.activeBody,'padding-left',bSelf.paddingLeft);
  bSelf.setStyle(bSelf.activeBody,'width',bSelf.balloonWidth);
  bSelf.setStyle(bSelf.activeBody,'z-index',10000);
  bSelf.setStyle(bSelf.activeText,'width',bSelf.balloonWidth - (bSelf.paddingLeft + bSelf.paddingRight));
  bSelf.setStyle(bSelf.activeText,'font-size',bSelf.balloonTextSize);

  // persistent balloons need a close control
  if (bSelf.balloonIsStatic) {
    if (vOrient == 'up') {
      bSelf.setStyle(bSelf.activeBody,'padding-top',7);
    }
    else {
      var margin = bSelf.isIE ? -4 : -8;
      bSelf.setStyle(bSelf.activeText,'margin-top',margin);
      bSelf.setStyle(bSelf.activeBody,'padding-top',1);
    }

    helpText = '\
    <a onClick="Balloon.prototype.hideStaticTooltip()" title="close this balloon" href=#\
    style="float:right;font-size:12px;text-decoration:none">\
    Close [X]</a><br>' + helpText;
  }
  else {
    if (vOrient == 'up') {
      bSelf.setStyle(bSelf.activeBody,'padding-top',bSelf.paddingTop);
    }
    else { 
      bSelf.setStyle(bSelf.activeBody,'padding-top',1);
      bSelf.setStyle(bSelf.activeText,'margin-top',0);
    }
  }


  // add the text to the caption layer
  bSelf.activeText.innerHTML = helpText;

  bSelf.showBalloon(vOrient,left,top,pageTop,pageBottom);
}

/////////////////////////////////////////////////////////////////////
// Convenience functions
/////////////////////////////////////////////////////////////////////

Balloon.prototype.getElement = function(id) {
  return document.getElementById(id);
}

// Set the active mouseover coordinates
Balloon.prototype.setActiveCoordinates = function(evt) {
  var el = this.getEventTarget(evt);
  var XY = this.eventXY(evt);
  // prefer element vertical bounds if available
  // otherwise, use event's
  this.activeTop  = this.getLoc('y1') || XY[1];
  this.activeTop -= 10;

  
  this.activeLeft = XY[0] - 10;

  this.activeRight = this.activeLeft + 20;
  this.activeBottom = this.getLoc('y2');
  if (this.activeBottom) this.activeBottom += 10;
  else this.activeBottom = this.activeTop + 20;
}

// Function based on example by Peter-Paul Koch
// http://www.quirksmode.org/js/events_properties.html
Balloon.prototype.eventXY = function(e) {
  var posx = 0;
  var posy = 0;
  if (!e) var e = window.event;
  if (e.pageX || e.pageY)       {
    posx = e.pageX;
    posy = e.pageY;
  }
  else if (e.clientX || e.clientY)      {
    posx = e.clientX + document.body.scrollLeft
                     + document.documentElement.scrollLeft;
    posy = e.clientY + document.body.scrollTop
                     + document.documentElement.scrollTop;
  }

  var XY = new Array(2);
  XY[0] = posx;
  XY[1] = posy;
  return XY;
}

// Function based on example by Peter-Paul Koch
// http://www.quirksmode.org/js/events_properties.html
Balloon.prototype.getEventTarget = function(e) {
  var targ;
  if (!e) var e = window.event;
  if (e.target) targ = e.target;
  else if (e.srcElement) targ = e.srcElement;
  if (targ.nodeType == 3) targ = targ.parentNode; // Safari
  return targ;
}

Balloon.prototype.setStyle = function(el,att,val) {
  if (att.match(/left|top|width|height|padding|margin/)) val += 'px'; 
  if (el) YAHOO.util.Dom.setStyle(el,att,val);
}

Balloon.prototype.getLoc = function(el,request) {
  var region = YAHOO.util.Dom.getRegion(el);
  switch(request) {
    case ('y1') : return region.top;
    case ('y2') : return region.bottom;
    case ('x1') : return region.left;
    case ('x2') : return region.right;
    case ('width')  : return (region.right - region.left);
    case ('height') : return (region.bottom - region.top);
    case ('region') : return region; 
 }
}

// We don't know if numbers are overridden with strings
Balloon.prototype.parseIntAll = function() {
  this.balloonWidth     = parseInt(this.balloonWidth);
  this.paddingTop       = parseInt(this.paddingTop);
  this.paddingLeft      = parseInt(this.paddingLeft);
  this.paddingRight     = parseInt(this.paddingRight);
  this.paddingBottom    = parseInt(this.paddingBottom);
  this.paddingConnector = parseInt(this.paddingConnector);
  this.vOffset          = parseInt(this.vOffset);
}

/////////////////////////////////////////////////////////////////////
// Create/append  balloon elements
/////////////////////////////////////////////////////////////////////

Balloon.prototype.createAndAppend = function(id,parent,elTag) {
  var node = this.justCreate(id,elTag);
  this.justAppend(node,parent);
  return node;
}

Balloon.prototype.justCreate = function(id,elTag) {
  var tag = elTag || 'div';
  var node = document.createElement(tag);
  node.setAttribute('id', id);  
  return node;
}

Balloon.prototype.justAppend = function(child,parent) {
  var parentNode = parent || document.body;
  parentNode.appendChild(child);
}


/////////////////////////////////////////////////////////////////////
// Balloon visibility controls
/////////////////////////////////////////////////////////////////////

Balloon.prototype.showBalloon = function(orient,left,top)  {
  YAHOO.util.Dom.setY(this.activeBalloon,999999999);
  this.setStyle(this.activeBalloon,'display','inline');

  if (orient == 'up') {
    var height = this.getLoc(this.activeBalloon,'height');
    top -= height;
  }

  YAHOO.util.Dom.setY(this.activeBalloon,top);
  YAHOO.util.Dom.setX(this.activeBalloon,left);
  balloonIsVisble = true;
  this.showHideSelect();
}

Balloon.prototype.hideTooltip = function() {
  var bSelf = currentBalloonClass;
  if (!bSelf) return;
  currentBalloonClass = null;
  window.clearTimeout(bSelf.timeoutTooltip);
  if (bSelf.balloonIsStatic) return false;
  balloonIsVisible = false;
  if (bSelf.activeBalloon) {
    bSelf.showHideSelect(1);
    bSelf.setStyle(bSelf.activeBalloon,'display','none');
  }
}

Balloon.prototype.hideStaticTooltip = function() {
  var bSelf = currentBalloonClass;
  currentBalloonClass = null;

  if (!bSelf) {
    var up   = document.getElementById('up');
    if (up) Balloon.prototype.setStyle(up,'display','none');
    var down = document.getElementById('down');
    if (down) Balloon.prototype.setStyle(down,'display','none');
  }
  else if (bSelf.activeBalloon) {
      bSelf.showHideSelect(1);
      bSelf.setStyle(bSelf.activeBalloon,'display','none');
  }	

  balloonIsVisible = false;
}

// to be called externally
Balloon.prototype.hideAllTooltips = function() {
  var bSelf = currentBalloonClass;
  if (!bSelf) return;
  currentBalloonClass = null;
  window.clearTimeout(this.timeoutTooltip);
  if (this.activeBalloon) this.setStyle(this.activeBalloon,'display','none');
  balloonIsVisble = false;
}

// IE select z-index bug
Balloon.prototype.showHideSelect = function(visible) {
  if (!this.isIE) return false;
  var sel = document.getElementsByTagName('select');
  if (!sel) return false;
  visible = visible ? 'visible' : 'hidden';
  for (var i=0; i<sel.length; i++) {
    if (this.isOverlap(sel[i]))
      this.setStyle(sel[i],'visibility',visible);
  }
}

Balloon.prototype.isOverlap = function(sel) {
  if (!this.activeBalloon) return false;
  var R1 = this.getLoc(this.activeBalloon,'region');
  var R2 = this.getLoc(sel,'region');
  var t1=R1.top,b1=R1.bottom,l1=R1.left,r1=R1.right;
  var t2=R2.top,b2=R2.bottom,l2=R2.left,r2=R2.right;

  if ( ((t2 < b1) && (t2 > t1)) 
      && (((l2 > l1) && (l2 < r1)) || (r2 < r1) && (r2 > l1))) return true;

  if ( ((b2 < b1) && (b2 > t1))
      && (((l2 > l1) && (l2 < r1)) || (r2 < r1) && (r2 > l1))) return true;

  return false;
}


///////////////////////////////////////////////////////
// AJAX widget to fill the balloons
// requires prototype.js
///////////////////////////////////////////////////////
Balloon.prototype.getContents = function(section) {
  // just pass it back if no AJAX handler is required.
  if (!this.helpUrl && !this.activeUrl) return section;
  
  // inline URL takes precedence
  var url = this.activeUrl || helpUrl;

  var pars = this.activeUrl ? '' : 'section='+section+';';

  var ajax  = new Ajax.Request( url,
                           { method:   'get',
                             asynchronous: false,
                             parameters:  pars,
                             onSuccess: function(t) { this.updateResult(t.responseText) },
                             onFailure: function(t) { alert('AJAX Failure! '+t.statusText)}});

  // activeUrl is meant to be single-use only
  this.activeUrl = null;

  return this.helpText || section;
}

Balloon.prototype.updateResult = function(text) {
  this.helpText = text;
}


