/*
 * ajaxutil.js -- AJAX-related utilities
 *
 * Requires XMLHttpRequest (or IE equivalent). Known to work correctly
 * in Firefox 1.0.7, Mozilla suite 1.7.5, Internet Explorer 6 (Windows)
 * and Safari 2.0.2.
 *
 * $Id: ajaxutil.js,v 1.10 2007/03/14 16:28:48 randy Exp $
 */

/**
 * IE doesn't define the DOM-standard constants on Node. Define the
 * ones we might use so we can actually use them.
 */
if ( ! Node )
{
   var Node =
   {
      ELEMENT_NODE:           1,
      ATTRIBUTE_NODE:         2,
      TEXT_NODE:              3,
      CDATA_SECTION_NODE:     4,
      DOCUMENT_NODE:          9,
      DOCUMENT_FRAGMENT_NODE: 11
   };
}

/**
 * IE5/Mac doesn't define undefined! We'll define it here, mostly
 * to avoid stupid parse errors, but it won't help much.
 */
try
{
   undefined;
}
catch ( e )
{
   var undefined = void 0;
}

/****************************************************************************
 * CONSTANTS
 ***************************************************************************/

/****************************************************************************
 * CONSTRUCTOR
 *
 * Everything is static, but if we don't have a constructor function,
 * we don't have an AjaxUtil class.
 ***************************************************************************/

function AjaxUtil()
{
}

/****************************************************************************
 * ASYNC LOADING SUPPORT
 ***************************************************************************/

/**
 * Load URL asynchronously and invoke client handler when done.
 *
 * An exception is thrown if the browser doesn't support XMLHttpRequest
 * (or its IE6 equivalent). A failure to load is not handled via 
 * exceptions, because there is no place for them to be reasonably
 * caught. Instead, if dispatchError is set to true, a faux request
 * object will be dispatched to the handler, with a null responseXML
 * but with status and statusText set per the failure. This allows the
 * client to examine the failure and act accordingly. If dispatchError
 * is set to false (the default), an alert() will be issued on failure.
 *
 * If loadURLInBackground() encounters an exception when calling
 * XMLHttpRequest.open(), most likely because of a cross-domain
 * scripting attempt, it dispatches a error with the special status
 * code of 601. This will be handled according to dispatchError,
 * as with real HTTP errors.
 *
 * By default, the content (MIME) type sent by the server will be
 * believed. This is important if you fetch XML data and expect the
 * request.responseXML DOM to be populated, because it won't be if
 * the server does something stupid like send "text/plain". To deal
 * with this, loadURLInBackground() lets you override the content
 * type with the expected MIME type (this works differently for Gecko
 * and IE, but should work okay for XML MIME types either way).
 *
 * @param url
 *    URL to be loaded
 * @param handler
 *    client handler to be invoked, of form function( url, request )
 * @param dispatchError
 *    if true, invoke handler with faux request on error; otherwise
 *    errors are reported to user through alert() 
 * @param contentType
 *    if defined, the content type of the URL as returned by the
 *    server will be overridden with the given type; if undefined
 *    (or false or null), the server will be believed
 * @param postData
 *    if defined, a POST will be done with this data; otherwise, a GET
 */
AjaxUtil.loadURLInBackground =
function( url, handler, dispatchError, contentType, postData )
{
   // create request using browser-appropriate method
   var request = false;
   if ( window.XMLHttpRequest )
   {
      try
      {
         request = new XMLHttpRequest();
      }
      catch ( e )
      {
         request = false;
      }
   }
   else if ( window.ActiveXObject )
   {
      try
      {
         request = new ActiveXObject( "Msxml2.XMLHTTP" );
      }
      catch ( e )
      {
         try
         {
            request = new ActiveXObject( "Microsoft.XMLHTTP" );
         }
         catch ( e )
         {
            request = false;
         }
      }
   }
   
   // throw an exception if we could not make the request
   if ( ! request )
   {
      var e = { type   : "XMLHttpRequest",
                URL    : url,
                message: "Unable to create XMLHttpRequest for this browser."
              }
      throw( e );
   }

   // invoke our own (private) handler when state changes; note that
   // the request object will be available through the magic of closures
   request.onreadystatechange = function ()
   {
      handleStateChange( url, request, handler );
   }

   // if given an explicit content type, set it (this works only for Gecko);
   // otherwise, we assume server is vending proper MIME type
   if ( contentType && request.overrideMimeType )
      request.overrideMimeType( contentType );

   // send off the request: if we have any postData, assume POST, else GET;
   // always turn off caching, because there is really no way for the user
   // to do a force reload of this underlying data stream (short of 
   // emptying the cache completely, an unpleasant alternative)
   try 
   {
      request.open( ( postData ? "POST" : "GET" ), url, true );
      request.setRequestHeader( "Cache-Control", "no-cache" );
      request.send( postData ? postData : null );
   }
   catch ( e )
   {
      if ( dispatchError )
      {
         // create faux request object with no data but status code
         var errorRequest = new Object();
         errorRequest.responseText = null;
         errorRequest.responseXML  = null;
         errorRequest.status       = 601;
         errorRequest.statusText   = "XMLHttpRequest Not Allowed";
         errorRequest.URL          = url;
         handler( url, errorRequest );
      }
      else
         alert( "Problem loading from URL " + url + " [601]" );
   }

   // private handler to response to state changes
   function handleStateChange( url, request, handler )
   {
      if ( request.readyState != 4 )
         return; // ignore non-completed states
      if ( request.status == 200 )
      {
         // if we got XML as text/plain or some other incorrect
         // MIME type from some braindead server (Netscape Enterprise 4,
         // I'm looking your way), we have to force IE to treat it as
         // XML by reloading the text into the (existing but empty)
         // DOM that is given; we then make a fake request object to
         // dispatch it back to the client (this is not needed in Gecko,
         // because gives us overrideMimeType() to deal with stupidity)
         var dispatchRequest = request;
         if ( window.ActiveXObject && contentTypeIsXML( contentType ) )
         {
            request.responseXML.loadXML( request.responseText );
            dispatchRequest = new Object();
            dispatchRequest.responseText = request.responseText;
            dispatchRequest.responseXML  = request.responseXML;
            dispatchRequest.status       = request.status;
         }
         handler( url, dispatchRequest );
      }
      else if ( dispatchError )
      {
         // create faux request object with no data but status code
         var errorRequest = new Object();
         errorRequest.responseText = null;
         errorRequest.responseXML  = null;
         errorRequest.status       = request.status;
         errorRequest.statusText   = request.statusText;
         errorRequest.URL          = url;
         handler( url, errorRequest );
      }
      else
         // can't throw an exception because there's no place to catch it!
         alert( "Problem loading from URL " + url
                + " [" + request.status + "]" );
   }

   function contentTypeIsXML( contentType )
   {
      // forced MIME types that we want to treat as XML
      return( contentType == "text/xml" 
              || contentType == "application/xml"
              || contentType == "application/xhtml+xml" );
   }
}

/****************************************************************************
 * PERIODIC EVENT SUPPORT
 *
 * Handling of periodic events, such as for animations and delayed
 * performs. We originally did this at the class-level because 
 * setTimeout() required a code string that ran at global scope, but
 * it turns out that modern browsers do support a function object
 * (see http://jibbering.com/faq/faq_notes/misc.html#mtSetTI).
 *
 * Anyway, we maintain the concept of an event manager that maintains
 * a list of event functions, which we continue to call until they
 * return false. However, there is a distinct manager object
 * (an AjaxUtil.PeriodicEvents object) for each interval, so we can
 * support events firing at different intervals. Use the class
 * method AjaxUtil.periodicEvents() to get (or make) the manager for
 * a given interval, e.g.:
 *
 *    AjaxUtil.periodicEvents( 500 ).addPeriodicEvent( handler );
 *
 ***************************************************************************/

AjaxUtil.periodicEventsByInterval = new Object();

/**
 * Get (or create) periodic event manager for given interval
 *
 * @param interval
 *    interval in milliseconds to get event manager for (default 100mS)
 * @returns
 *    appropriate AjaxUtil.PeriodicEvents object
 */
AjaxUtil.periodicEvents = function( interval )
{
   if ( ! interval )
      interval = 100;
   var mgr = AjaxUtil.periodicEventsByInterval[ interval ];
   if ( ! mgr )
   {
      mgr = new AjaxUtil.PeriodicEvents( interval );
      AjaxUtil.periodicEventsByInterval[ interval ] = mgr;
   }
   return( mgr );
}

/**
 * AjaxUtil.PeriodicEvents constructor
 *
 * @param interval
 *    interval in milliseconds to get event manager for (default 100mS)
 */
AjaxUtil.PeriodicEvents = function( interval )
{
   this.periodicList     = new Array();
   this.periodicID       = null;
   this.periodicInterval = ( interval ? interval : 100 );
}

/**
 * Start any periodic events previously scheduled with addPeriodicEvent().
 */
AjaxUtil.PeriodicEvents.prototype.startPeriodicEvents = function()
{
   if ( ! this.periodicID && this.periodicList.length != 0 )
   {
      var _this = this;
      var timeoutFunc = function()
      {
         _this.performPeriodicEvents();
      };
      timeoutFunc.toString = function()
      {
         // do nothing in browsers that don't accept function object
         return( ";" );
      }
      this.periodicID = setTimeout( timeoutFunc, this.periodicInterval );
   }
}

/**
 * Stop all current periodic events.
 *
 * The active periodic events are retained in the scheduling list, and
 * can be restarted later with startPeriodicEvents().
 */
AjaxUtil.PeriodicEvents.prototype.stopPeriodicEvents = function()
{
   if ( this.periodicID )
   {
      clearTimeout( this.periodicID );
      this.periodicID = null;
   }
}

/**
 * Schedule a new periodic event, and start if necessary.
 *
 * @param periodic
 *    periodic event to add, as function taking no arguments and returning
 *    true to be rescheduled or false to be terminated
 */
AjaxUtil.PeriodicEvents.prototype.addPeriodicEvent = function( periodic )
{
   this.periodicList.push( periodic );
   this.startPeriodicEvents();
}

/**
 * Perform all active periodic events.
 *
 * This is intended to be called from setTimeout() on regular intervals.
 * It will run all events that are currently scheduled, retaining
 * those that return true, to be used for the next cycle. It then reschedules
 * itself if there are any events remaining.
 */
AjaxUtil.PeriodicEvents.prototype.performPeriodicEvents = function()
{
   this.periodicID = null;

   if ( this.periodicList.length == 0 )
      return;

   // accumulate the periodic events that return true, meaning that
   // the periodic event is still valid and should be rescheduled
   var nextRound = new Array();
   for ( var a = 0 ; a < this.periodicList.length ; ++a )
   {
      var periodic = this.periodicList[ a ];
      if ( periodic() )
         nextRound.push( periodic );
   }

   // reinstall the new list, and set next timeout if anything to do
   this.periodicList = nextRound;
   this.startPeriodicEvents();
}

/****************************************************************************
 * EFFECT SUPPORT BASED on moo.fx
 ***************************************************************************/

/**
 * Replace one element for another using a pair of moo.fx effects.
 *
 * Use the "basic effects" of the moo.fx library, http://moofx.mad4milk.net/,
 * to do a simple transition between two children of a given container.
 * The outbound element will be transitioned out and removed from the
 * parent container, and then the inbound element will be added to the
 * same container and transitioned in. The default effect is Opacity, to
 * achieve a cross-fade effect, but another basic effect can be specified.
 *
 * One of the two elements can be undefined, in which case there will
 * be only a one-way transition (e.g. to or from blank).
 *
 * If the moo.fx library has not been loaded, this will revert to a simple
 * (and immediate) swapping of elements with no effects.
 *
 * Note: For IE, the moo.fx Opacity effect uses the "alpha(opacity=x)"
 * filter [versus the CSS3 opacity property elsewhere]. This works only
 * if the element being effected has "layout". Per the following page
 * http://msdn.microsoft.com/workshop/author/dhtml/reference/properties/
 * haslayout.asp, the following properties give an element "layout":
 * 
 *    CSS property   Value                Caveats
 *    -----------    ------------         -----------------------
 *    display        inline-block
 *    height         any value            but not auto
 *    float          left or right
 *    position       absolute
 *    width          any value            but not auto
 *    writing-mode   tb-rl
 *    zoom           any value            
 *
 * But even though setting one of these does cause the effect to take,
 * they all seem to cause the text to be drawn hideously (IE6/Win).
 *
 * @param container
 *    element containing the elements to be swapped
 * @param outElem
 *    outbound element; should already be a child of container
 * @param inElem
 *    inbound element; will be child of container on completion
 * @param fxName
 *    moo.fx transition to use; default is Opacity
 * @param fxTime
 *    total transition time; default of 200ms
 * @param onElementOut
 *    handler to call after outElem is transitioned out
 * @param onElementIn
 *    handler to call after inElem is transitioned in
 */
AjaxUtil.transitionFX = function( container,
                                  outElem, inElem,
                                  fxName,
                                  fxTime,
                                  onElementOut, onElementIn )
{
   // set default effect type and time if not set
   if ( ! fxName )
      fxName = "Opacity";
   if ( ! fxTime )
      fxTime = 200;

   // bail if no elements to work with
   if ( ! outElem && ! inElem )
      return;

   // if the moo.fx library was not properly loaded, just do a 
   // transition-less exchange of elements and bail
   if ( ! window.fx || ! fx[ fxName ] )
   {
      if ( outElem )
      {
         container.removeChild( outElem );
         if ( onElementOut )
            onElementOut();
      }
      if ( inElem )
      {
         container.appendChild( inElem );
         if ( onElementIn )
            onElementIn();
      }
      return;
   }

   // if two elements, we have an effect in each direction
   var fxIn;
   var fxOut;

   // create the inbound element effect, which starts out hidden
   if ( inElem )
   {
      fxIn = new fx[ fxName ]( inElem, { duration: ( fxTime / 2 ),
                                         onComplete: onElementIn } );
      fxIn.hide();
   }

   // if we have no outbound element, just toggle in the inbound one
   if ( ! outElem )
   {
      container.appendChild( inElem );
      if ( onElementOut )
         onElementOut();
      fxIn.toggle();
      return;
   }

   // create the outbound element effect, which upon completion will
   // trigger the inbound element effect to begin
   var onDoneOut = function()
   {
      if ( outElem.parentNode == container )
         container.removeChild( outElem );
      if ( onElementOut )
         onElementOut();
      if ( inElem )
         container.appendChild( inElem );
      if ( fxIn )
         fxIn.toggle();
   }
   fxOut = new fx[ fxName ]( outElem, { duration: ( fxTime / 2 ),
                                        onComplete: onDoneOut } );

   // start the whole business by toggling the outbound element out
   fxOut.toggle();
}

/****************************************************************************
 * XML CONVENIENCE UTILITIES
 ***************************************************************************/

AjaxUtil.XML = function()
{
}

/**
 * Extract only the text node content from an element and its children
 *
 * This function extracts both text node and CDATA node content,
 * and concatenates it together to return a single string. No characters
 * are inserted between text from adjacent elements; leading and trailing
 * whitespace is trimmed (possibly resulting in an empty string).
 *
 * @param elem
 *    the element to extract the text from
 * @returns
 *    all the text content from elem and children, concatenated
 */
AjaxUtil.XML.getTextContent = function( elem )
{
   var text = '';
   var children = elem.childNodes;
   for ( var i = 0 ; i < children.length ; ++i )
   {
      var child = children[ i ];
      if ( child.nodeType == Node.TEXT_NODE 
           || child.nodeType == Node.CDATA_SECTION_NODE )
         text += child.data;
      else
         text += this.getTextContent( child );
   }

   // strip leading and trailing whitespace
   text = text.replace( /^\s+/, '' );
   text = text.replace( /\s+$/, '' );
   
   return( text );
}

/****************************************************************************
 * USER AGENT-SPECIFIC BEHAVIOR ABSTRACTION
 ***************************************************************************/

/**
 * Properties to identify specific user agent characteristics.
 *
 * This object, AjaxUtil.UserAgent, is an attempt to encapsulate
 * agent-specific hacks by their behavior rather than by their 
 * identity. So instead of a lot of code saying "do x if using
 * IE5/Win," the code can say "do x if broken box model" instead.
 * Then, only *this* code needs to track actual browser behavior.
 */
AjaxUtil._UserAgent = function()
{
   var agent = navigator.userAgent;

   // check for WebKit versions
   var webkit = undefined;
   var wkver = agent.indexOf( "AppleWebKit/" );
   if ( wkver != -1 )
      webkit = parseInt( agent.substr( wkver + "AppleWebKit/".length ) );

   // check for IE versions
   var ie     = ( agent.indexOf( "MSIE" ) != -1
                  && agent.indexOf( "Windows" ) != -1 );
   var ie55   = ( ie && agent.indexOf( "MSIE 5.5" ) != -1 );

   // check for Gecko versions
   var mozmac = ( agent.indexOf( "Gecko" ) != -1
                  && agent.indexOf( "Macintosh" ) != -1 );

   // current versions of WebKit don't repaint a float inside a
   // fixed height element (when added via DOM scripting) unless
   // a layer exists for the float (e.g. non-static positioning);
   // see OpenDarwin Bug 7204; fixed in version 420
   this.floatInFixedHeightRequiresPosition = ( webkit <= 418 );

   // IE5.5/Win has that wonderful thing where it subtracts padding from
   // width and height
   this.hasBrokenBoxModel = ie55;

   // IE5.5/Win doesn't support auto-margin centering, but does
   // center (non-text) with text-align
   this.textCenterForNonText = ie55;

   // IE5.5/Win doesn't seem to understand how to apply padding to
   // an IMG element, and if you add the padding to the width/height
   // (as the broken box model suggests), it just scales the image
   this.ignoresPaddingForIMG = ie55;

   // IE6/Win does an opacity fade by exposing whatever is on the body
   // element, even if there is another (opaque) element in front
   this.bodyVisibleOnTransparent = ie;

   // IE6/Win doesn't support window.innerHeight; by experiment, it
   // seems that document.documentElement.offsetHeight gives
   // the actual window height (even if the actual body element
   // is shorter than the window)
   this.getInnerHeight = function()
   {
      return( document.documentElement
              ? document.documentElement.offsetHeight : undefined );
   };

   // IE6/Win requires that the style.width/height properties be set
   // to actually scale an IMG element (the IMG's width and height
   // alone don't seem to do it for IE)
   this.imgResizeRequiresCSSProps = ie;

   // IE6/Win has stupid non-standard names for CSS float
   this.nonStandardCSSFloat = ( ie ? "styleFloat" : undefined );

   // Firefox on Mac (as of 2.0.0.1) does this weird thing where a
   // scrollbar from overflow: auto is drawn above an element that
   // has a higher z-index; but it only seems to happen on MacOSX
   this.drawsScrollWithMaxZIndex = mozmac;
}
AjaxUtil.UserAgent = new AjaxUtil._UserAgent();

/****************************************************************************
 * DEBUGGING UTILITIES
 ***************************************************************************/

/**
 * Issue log message to FireBug Console pane.
 *
 * If FireBug <http://www.joehewitt.com/software/firebug> is installed,
 * issue a message to its Console pane. This function is copied directly
 * from the FireBug faq; as it says: the function takes an optional
 * second argument which is the name of the tab to select. Tab names
 * include "xml", "css", "box", "capture", or "js".
 *
 * The function (or event?) name "printfire" seems to be hardwired into
 * FireBug somehow, because I can't change it and still have it work.
 * It is aliased to AjaxUtil.logFire() for consistency, though.
 */
function printfire()
{
   if ( document.createEvent )
   {
      printfire.args = arguments;
      var evt = document.createEvent( 'Events' );
      evt.initEvent( 'printfire', false, true );
      dispatchEvent( evt );
   }
}
AjaxUtil.logFire = printfire;

