// ToolTip class

// Common event handler API code.
if (typeof addEvent != 'function')
{
 var addEvent = function(o, t, f, l)
 {
  var d = 'addEventListener', n = 'on' + t, rO = o, rT = t, rF = f, rL = l;
  if (o[d] && !l) return o[d](t, f, false);
  if (!o._evts) o._evts = {};
  if (!o._evts[t])
  {
   o._evts[t] = o[n] ? { b: o[n] } : {};
   o[n] = new Function('e',
    'var r = true, o = this, a = o._evts["' + t + '"], i; for (i in a) {' +
     'o._f = a[i]; r = o._f(e||window.event) != false && r; o._f = null;' +
     '} return r');
   if (t != 'unload') addEvent(window, 'unload', function() {
    removeEvent(rO, rT, rF, rL);
   });
  }
  if (!f._i) f._i = addEvent._i++;
  o._evts[t][f._i] = f;
 };
 addEvent._i = 1;
 var removeEvent = function(o, t, f, l)
 {
  var d = 'removeEventListener';
  if (o[d] && !l) return o[d](t, f, false);
  if (o._evts && o._evts[t] && f._i) delete o._evts[t][f._i];
 };
}

function cancelEvent(e, c) {
	e.returnValue = false;
	if (e.preventDefault) e.preventDefault();
	if (c) {
		e.cancelBubble = true;
		if (e.stopPropagation) e.stopPropagation();
	}
};

// *** MAIN OBJECT CLASS ***
function ToolTip(myName, config) {
	var defaults = {
		myName: myName,        // Names used for notes of this class.
		allowNesting: false,   // Whether to allow triggers within triggers.
		cssProp: 'visibility', // CSS property used to show/hide notes and values.
		cssVis: 'inherit',
		cssHid: 'hidden',
		IESelectBoxFix: true,  // Enables the IFRAME select-box-covering fix.
		mouseX: 0, mouseY: 0,  // Live mouse co-ords.
		notes: {},             // Store for note timers/references.
		rootElm: null,         // The outermost element, handles mouse events.
		onshow: null,          // Events you can capture. Passed the note ID.
		onhide: null
	};

	for (var p in defaults)
		this[p] = (typeof config[p] == 'undefined') ? defaults[p] : config[p];

	// Capture mouse events on the entire document.
	var obj = this;
	//addEvent(document, 'mouseover', function(evt) { obj.mouseHandler(evt, 1) } );
	addEvent(document, 'click', function(evt) { obj.mouseHandler(evt, 2) } );
	//addEvent(document, 'mouseout', function(evt) { obj.mouseHandler(evt, 0) } );

	// Record this instance in our 'instances' array (for setTimeouts).
	this.instance = ToolTip.instances.length;
	ToolTip.instances[this.instance] = this;
}
// Collection of all objects created.
ToolTip.instances = [];
// A list of note behaviour and positioning type handlers.
ToolTip.prototype.bTypes = {};
ToolTip.prototype.pTypes = {};


// *** DEFAULT TYPE HANDLERS ***

// Handlers are called as the note's state changes. They are passed:
// 1) An object reference to this ToolTip object.
// 2) The ID of the note in question.
// 3) The proposed next 'visible' flag of the note.
// 4) The proposed next 'animating' flag of the note ("positional" only).
//
// There are two types: "pTypes" are positional handlers, "bTypes" behavioural.
// Use positional handlers to perform actions when notes show/hide.
// Use behavioural handlers to cancel default actions; they're called when
// notes are about to animate, and can "return false" to cancel it.

// Find the trigger position and offset the note from that.
ToolTip.prototype.pTypes.triggeroffset = function(obj, noteID, nextVis, nextAnim) { with (obj) {
	var note = notes[noteID];
	if (nextVis && !note.animating && !note.visible) {
		var x = 0, y = 0, elm = note.trigRef;
		x = elm.width;
		y = -elm.height;
		while (elm) {
			//alert(getStyle(elm,'position'));
			//if (elm.getAttribute('id') != 'big_picture') {
			if (getStyle(elm,'position') != 'relative') {
				x += elm.offsetLeft;
				y += elm.offsetTop;
			}
			elm = elm.offsetParent;
		}
		note.ref.style.left = checkWinX(x, note) + 'px';
		note.ref.style.top = checkWinY(y, note) + 'px';
	}
}};

function getStyle(oElm, strCssRule) {
	var strValue = "";

	if(document.defaultView && document.defaultView.getComputedStyle) {
		strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
	} else if(oElm.currentStyle) {
		try {
			strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1) { return p1.toUpperCase(); });
			strValue = oElm.currentStyle[strCssRule];
		} catch(e) {	// used to prevent an error in IE 5
			strValue = '';
		}
	}
	return strValue;
}

// Ignore hide requests.
ToolTip.prototype.bTypes.pinned = function(obj, noteID, nextVis) { with (obj) {
	return (!nextVis) ? false : true;
}};


// *** OBJECT METHODS ***
ToolTip.prototype.docBody = function()
{
 return document[(document.compatMode &&
  document.compatMode.indexOf('CSS') > -1) ? 'documentElement' : 'body'];
};
ToolTip.prototype.getWinW = function()
{
 return this.docBody().clientWidth || window.innerWidth || 0;
};
ToolTip.prototype.getWinH = function()
{
 return this.docBody().clientHeight || window.innerHeight || 0;
};
ToolTip.prototype.getScrX = function()
{
 return this.docBody().scrollLeft || window.scrollX || 0;
};
ToolTip.prototype.getScrY = function()
{
 return this.docBody().scrollTop || window.scrollY || 0;
};

// Takes a note and a proposed X position, and modifies the position so that
// the note will fit within the border of the visible browser window.
ToolTip.prototype.checkWinX = function(newX, note) { with (this) {
	return Math.max(getScrX(), Math.min(newX, getScrX() + getWinW() - note.ref.offsetWidth - 8));
}};

ToolTip.prototype.checkWinY = function(newY, note) { with (this) {
	return Math.max(getScrY(), Math.min(newY, getScrY() + getWinH() - note.ref.offsetHeight - 8));
}};


ToolTip.prototype.mouseTrack = function(evt) { with (this)
{
 // Stores the mouse coordinates as it moves.

 mouseX = evt.pageX || evt.clientX + getScrX() || 0;
 mouseY = evt.pageY || evt.clientY + getScrY() || 0;
}};

// Optional custom note "close" button handler extension used in this example.
// This picks up click on CLASS="note-close" elements within CLASS="snb-pinned"
// notes, and closes the note when they are clicked.
// It can be deleted if you're not using it.
ToolTip.prototype.clickClose = function(evt) { with (this) {
	var elm = evt.target || evt.srcElement, closeBtn, note;
	
	while (elm) {
		if ((/note-close/).test(elm.className)) closeBtn = elm;
		if ((/snb-pinned/).test(elm.className)) { note = elm; break }
		elm = elm.parentNode;
	}
	
	if (closeBtn && note) {
		var noteData = note.id.match(/([a-z_\-0-9]+)-note-([a-z_\-0-9]+)/i);
		for (var i = 0; i < ToolTip.instances.length; i++)
			if (ToolTip.instances[i].myName == noteData[1]) {
				setTimeout('ToolTip.instances[' + i + '].setVis("' + noteData[2] + '", false, true)', 100);
				cancelEvent(evt);
			}
	}
}};


ToolTip.prototype.mouseHandler = function(evt, show) { with (this)
{
 // This is called from an onmouseover/click/mouseout handler in the document.
 // show: 0 = mouseout, 1 = mouseover, 2 = click.
 // It loops up the DOM from the event source, initialises element references,
 // and forwards the event to display() for processing.

 if (!document.documentElement) return true;

 var srcElm = evt.target || evt.srcElement,
  // Match CLASS="objectname-hover-foobar" or "objectname-click-foobar".
  trigRE = new RegExp(myName + '-(hover|click)-([a-z0-9]+)', 'i'),
  // Match ID="objectname-target-foobar"
  targRE = new RegExp(myName + '-(note)-([a-z0-9]+)', 'i'),
  // Whether to repsect a found trigger or not (for nesting triggers).
  trigFind = 1,
  // A list of found notes in the current DOM tree.
  foundNotes = {};

 if (srcElm.nodeType != 1) srcElm = srcElm.parentNode;
 var elm = srcElm;
 while (elm && elm != rootElm)
 {
  // Match our regexes, and keep going upwards to detect nested notes!
  if (targRE.test(elm.id) || (trigFind && trigRE.test(elm.className)))
  {
   if (!allowNesting) trigFind = 0;
   var click = RegExp.$1 == 'click' ? 1 : 0,
    noteID = RegExp.$2,
    ref = document.getElementById(myName + '-note-' + noteID),
    trigRef = trigRE.test(elm.className) ? elm : null;

   // If the target exists...
   if (ref)
   {
    // Init data store, keyed on the last part of the tag class/id.
    if (!notes[noteID])
    {
     notes[noteID] = {
      click: click,
      ref: ref,
      trigRef : null,
      visible: 0,
      animating: 0,
      timer: null
     };
     // Also store references in the note DOM object.
     ref._sn_obj = this;
     ref._sn_id = noteID;
    }
    var note = notes[noteID];

    // Add to our list of found notes on this loop up the DOM.
    // Don't "find" clicks on triggers; they're 'outside' clicks, hiding note.
    if (!note.click || (trigRef != srcElm)) foundNotes[noteID] = true;

    // Show/hide hover notes, and show onclick notes.
    if (!note.click || (show == 2))
    {
     // If this is a trigger, record this as the note's current trigger.
     // Also record the trigger reference on the note DOM object for looping.
     if (trigRef)
      notes[noteID].trigRef = notes[noteID].ref._sn_trig = elm;
     // Now call display() to show/record it, if its visibility has changed.
     display(noteID, show);
     // Click notes: avoid navigating the entire document :).
     if (note.click && (srcElm == trigRef)) cancelEvent(evt);
    }
   }
  }

  // Otherwise check the next element up the DOM.
  if (elm._sn_trig)
  {
   // If this is a note target, recurse to its original trigger and allow
   // further triggers to be found and activated.
   trigFind = 1;
   elm = elm._sn_trig;
  }
  else
  {
   elm = elm.parentNode;
  }
 }

 // Now we've finished looping up the DOM, do a final loop through our list
 // of notes, checking to see if any onclick notes need hiding.
 if (show == 2) for (var n in notes)
 {
	 //alert(' '+n+' * '+notes[n].click+' * '+notes[n].visible+' * '+foundNotes[n]);
  if (notes[n].click && notes[n].visible && !foundNotes[n] && (noteID != n)) display(n, 0);
 }
}};


ToolTip.prototype.display = function(noteID, show) { with (this)
{
 // Called to manage show/hide timers for a specified target.
 with (notes[noteID])
 {
  // Clear any existing timeout.
  clearTimeout(timer);

  // Check that this note isn't already performing the desired action.
  if (!animating || (show ? !visible : visible)) {
	ToolTip.instances[instance].setVis(noteID, show, true);
  }
 }
}};


// Processes the two 'types' for each note and returns the results.
// The behavioural type is called when about to animate; it can "return false" to cancel the pending action.
// The positioning type is always called whenever an action succeeds.
ToolTip.prototype.checkType = function(noteID, nextVis, nextAnim) { with (this) {
	var note = notes[noteID], bType, pType;

	if ((/snp-([a-z]+)/).test(note.ref.className)) pType = RegExp.$1;
	if ((/snb-([a-z]+)/).test(note.ref.className)) bType = RegExp.$1;

	if (nextAnim && bType && bTypes[bType] && (bTypes[bType](this, noteID, nextVis) == false)) return false;
	if (pType && pTypes[pType]) pTypes[pType](this, noteID, nextVis, nextAnim);
	return true;
}};


ToolTip.prototype.setVis = function(noteID, show, now) { with (this)
{
 // Called from display() above; sets the note visibility,
 // calls the type handler function, and calls animate() below.
 // Pass 'now' in order to show/hide immediately (skips animation).

 var note = notes[noteID];
 if (note && checkType(noteID, show, 1) || now)
 {
  note.visible = show;
  note.animating = 1;
  animate(noteID, show, now);
 }
}};


ToolTip.prototype.animate = function(noteID, show, now) { with (this)
{
	//now = true;	// probably safe to set true
 // Repeatedly called to animate a note in and out (if applicable).

 var note = notes[noteID];

 if (!note.animTimer) note.animTimer = 0;
 if (!note.animC) note.animC = 0;

 with (note)
 {
  clearTimeout(animTimer);

  if (show)
  {
   if (onshow) this.onshow(noteID);
   IEFrameFix(noteID, 1);
   ref.style[cssProp] = cssVis;
  }

  animC = Math.max(0, Math.min(1, (show ? 1 : -1)));

//alert(show+' * '+animC);
  if (!show)
  {
   if (onhide) this.onhide(noteID);
   IEFrameFix(noteID, 0);
   ref.style[cssProp] = cssHid;
  }

//  if (animC != parseInt(animC))
//  {
//   animTimer = setTimeout(myName + '.animate("' +
//    noteID + '", ' + show + ')', 50);
//  }
//  else
//  {
   checkType(noteID, animC ? 1 : 0, 0);
   note.animating = 0;
//  }
 }
}};


// Positions a hidden IFRAME under a note to allow it to cross over <select> boxes in MSIE.
ToolTip.prototype.IEFrameFix = function(noteID, show) { with (this) {
	if (!window.createPopup || !IESelectBoxFix) return;

	var note = notes[noteID], ifr = note.iframe;
	if (!ifr) {
		ifr = notes[noteID].iframe = document.createElement('iframe');
		ifr.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)';
		ifr.style.position = 'absolute';
		ifr.style.borderWidth = '0';
		note.ref.parentNode.insertBefore(ifr, note.ref.parentNode.firstChild);
	}

	if (show) {
		ifr.style.left = note.ref.offsetLeft + 'px';
		ifr.style.top = note.ref.offsetTop + 'px';
		ifr.style.width = note.ref.offsetWidth + 'px';
		ifr.style.height = note.ref.offsetHeight + 'px';
		ifr.style.visibility = 'inherit';
	} else {
		ifr.style.visibility = 'hidden';
	}
}};

addEvent(document, 'click', function(evt) {
	var elm = evt.target || evt.srcElement, closeBtn, note;
	
	while (elm) {
		if ((/note-close/).test(elm.className)) closeBtn = elm;
		if ((/snb-pinned/).test(elm.className)) { note = elm; break }
		elm = elm.parentNode;
	}
	
	if (closeBtn && note) {
		var noteData = note.id.match(/([a-z_\-0-9]+)-note-([a-z_\-0-9]+)/i);
		for (var i = 0; i < ToolTip.instances.length; i++)
			if (ToolTip.instances[i].myName == noteData[1]) {
				setTimeout('ToolTip.instances[' + i + '].setVis("' + noteData[2] + '", false, true)', 100);
				cancelEvent(evt);
			}
	}
});

