// featureTesting.js
(function() {
  var global = this;
  var fs = [];
  var running = false;

  global.FORK = function(prop) {
    // cannot be reentrant because if a lazy test function makes another call to FORK() for an undefined property
    //  then infinite loop
    if (running) {
      return;
    }
    try {
      running = true;

      if (typeof prop == 'string') {
        if (!(arguments.callee.hasOwnProperty(prop))) {
          for (var i=0, ilen=fs.length; i<ilen; i++) {
            fs[i]();
          }
        }
        if (arguments.callee.hasOwnProperty(prop)) {
          return arguments.callee[prop];
        }
      }
      else if (typeof prop == 'function') {
        fs.push(prop);
      }
      else {
        throw new TypeError('TODO message');
      }
      
    }
    catch(e) {}
    running = false;
  };
  
})();


(function() {
  
  // Use for testing if a DOM property is present.
  // Use LIB.isHostCollection if the property is
  // a collection. Use LIB.isHostMethod if
  // you will be calling the property.
  //
  // Examples:
  //   // "this" is global/window object
  //   LIB.isHostObject(this, 'document');
  //   // "el" is some DOM element
  //   LIB.isHostObject(el, 'style');
  FORK.isHostObject = function(o, p) {
    return !!(typeof(o[p]) == 'object' && o[p]);
  };

  // Use for testing if DOM collects are present
  // Some browsers make collections callable and
  // have a typeof 'function'.
  //
  // Examples:
  //   // since tested document as property of "this"
  //   // need to refer to it as such
  //   var doc = this.document;
  //   LIB.isHostCollection(doc, 'all');
  //   // "el" is some DOM element
  //   LIB.isHostCollection(el, 'childNodes');
  FORK.isHostCollection = function(o, m) {
    var t = typeof(o[m]);
    return (!!(t=='object' && o[m])) ||
           t=='function';
  };

  // Use for testing a DOM property you intend on calling.
  // In Internet Explorer, some ActiveX callable properties
  // have a typeof "unknown". IE returns "object" for typeof
  // operations on callable host objects, but other browsers
  // (e.g. Safari, Opera) return "function."
  //
  // Examples:
  //   // since tested document as property of "this"
  //   // need to refer to it as such
  //   var doc = this.document;
  //   LIB.isHostMethod(doc, 'createElement');
  //   // "el" is some DOM element
  //   LIB.isHostMethod(el, 'appendChild');
  FORK.isHostMethod = function(o, m) {
    var t = typeof(o[m]);  
    return t=='function' ||
           !!(t=='object' && o[m]) ||
           t=='unknown';
  };

  if (!(FORK.isHostObject(this, 'document'))) {
    return;
  }
  var doc = this.document;
  
  FORK.getDocument = function() {
    return doc;
  };
  
  if (FORK.isHostObject(doc, 'documentElement')) {
    FORK.getAnElement =
    FORK.getDocumentElement =
      function(d) {
        return (d || doc).documentElement;
      };
  }

})();

// dom.js
(function() {

  // IE 5.01 has regular expressions but they are not powerful enough.
  // Try one here that has all the power required for this lib to run
  // and browsers without enough power will show a syntax error as this page loads
  // and FORK and FORK.hasClass will never be defined
  var re = /(?:^|\s+)a(?:\s+|$)/g, // TODO can drop the ?: and maybe Opera problem below goes away, Maybe used RegExp constructor to avoid syntax error?
      el;
      
  if (typeof FORK == 'function' &&
      FORK.isHostObject &&
      FORK.isHostMethod &&
      FORK.getAnElement &&
      (el = FORK.getAnElement()) &&
      typeof el.className == 'string' &&
      (typeof RegExp == 'function') &&
      (typeof String.prototype.replace == 'function') &&
      (typeof String.prototype.match == 'function') &&
      "a".match(re) // Opera 6.0.6 doesn't syntax error on the regular expression above but this match will return null
     ) {
      
    var hasClass = function(el, className) {
      return (new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)')).test(el.className);
    };

    var addClass = function(el, className) {
      if (!hasClass(el, className)) {
        el.className += ' ' + className;
      }
    };

    var removeClass = function(el, className) {
      if (hasClass(el, className)) {
        el.className = el.className.replace(
          (new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)', 'g')),
          ' ');
        removeClass(el, className); // in case of multiple adjacent of same class name being removed
      }
    };

    var makeIter = function(fn) {
      return function(els, className) {
        if (!(els instanceof Array)) { // TODO don't use instanceof in case mutliple windows/frames involved
          els = [els];
        }
        for (var i=0, ilen=els.length; i<ilen; i++) {
          fn(els[i], className);
        }
      };
    };

/**

@property FORK.addClass

@parameter els A DOM element or an array of DOM elements.

@parameter className Each element in <code>els</code> will have the class name added if
it does not have the class name already.

@description
<pre><code>
FORK.addClass(document.getElementById('foo'), 'bar');
FORK.addClass([el1, el2], 'bar');
</code></pre>

*/
    FORK.addClass = makeIter(addClass);

/**

@property FORK.removeClass

@parameter els A DOM element or an array of DOM elements.

@parameter className Each element in <code>els</code> will have the class name removed.

@description
<pre><code>
FORK.removeClass(document.getElementById('foo'), 'bar');
FORK.removeClass([el1, el2], 'bar');
</code></pre>

*/
    FORK.removeClass = makeIter(removeClass);
    
/**

@property FORK.hasClass

@parameter els A DOM element, or an array of DOM elements.

@parameter className Each element in <code>els</code> will be tested to have this class name.

@description
<pre><code>
FORK.hasClass(document.getElementById('foo'), 'bar');
FORK.hasClass([el1, el2], 'bar');
</code></pre>

*/
    FORK.hasClass = function(els, className) {
      if (!(els instanceof Array)) { // TODO don't use instance of in case multiple windows/frames involved
        els = [els];
      }
      for (var i=0, ilen=els.length; i<ilen; i++) {
        if (!hasClass(els[i], className)) {
          return false;
        }
      }
      return true;
    };
    
  } // if feature tests
  
})();

// mutate.js
(function() {
  var global = this,
      b,
      doc;
  
  // TODO need to test for may other things (tagName, childNodes, parentNode, etc)
  
  // feature test
  if (typeof FORK == 'function' && // TODO FORK is a function now?
      FORK.isHostObject &&
      FORK.isHostMethod &&
      FORK.isHostObject(global, 'document') &&
      (doc = global.document) && // yes just one equals sign
      FORK.isHostMethod(doc, 'createElement') &&
      (b = doc.createElement('div')) && // yes just one equals sign
      (typeof b.innerHTML == 'string') &&
      (typeof String.prototype.replace == 'function')) {
  
    var scriptRegExp = /<script.*?>((\n|\r|.)*?)<\/script>/img;

    var getScripts = function(html) {
      var ss = [], // an array of found scripts
          match;
      while (match = scriptRegExp.exec(html)) { // yes just one equals sign
        ss.push(match[1]);
      }
      return ss;
    };

    var stripScripts = function(html) {
      return html.replace(scriptRegExp, '');
    };

    // takes one string argument which is the script to be evaluated. 
    var evalScript = function() {
      // this function doesn't declare any variable names so there is minimal 
      // chance of clash with the evaluated script's variables.
      eval(arguments[0]);
    };

    var evalScripts = function(scripts) {
      for (var i=0, l=scripts.length; i<l; i++) {
        evalScript(scripts[i]);
      }
    };

    // used by replace, insertBefore, and insertAfter
    var outsideParser = function(el, html) {
      var p = document.createElement('div'), // TODO use "doc" and allow optional argument for which document to use
                                             // (determine which document to use based on which element is being replaced etc)
      tagName = el.tagName.toLowerCase();

      if (tagName.match(/t(body|head|foot)/)) { // must be trying to replace the tbody, thead, or tfoot
        p.innerHTML = '<table>' + html + '</table>';
        p = p.childNodes[0];
      } else if (tagName == 'tr') { // must be trying to replace one row with one or more rows
        p.innerHTML = '<table><tbody>' + html + '</tbody></table>';
        p = p.childNodes[0].childNodes[0];
      } else if (tagName == 'td') { // must be trying to replace one data cell with one or more cells
        p.innerHTML = '<table><tbody><tr>' + html + '</tr></tbody></table>';
        p = p.childNodes[0].childNodes[0].childNodes[0];
      } else {
        p.innerHTML = html;
      }

      return p;
    };
  
    // used by insertTop and insertBottom
    var insideParser = function(el, html) {
      var p = document.createElement('div'),
          tagName = el.tagName.toLowerCase();

      if (tagName == 'table') { // must be trying to insert tbody, thead, or tfoot
        p.innerHTML = '<table>' + html + '</table>';
        p = p.childNodes[0];
      } else if (tagName.match(/t(head|body|foot)/)) { // must be trying to insert rows.
        p.innerHTML = '<table><tbody>' + html + '</tbody></table>';
        p = p.childNodes[0].childNodes[0];
      } else if (tagName == 'tr') { // must be trying to insert some td elements
        p.innerHTML = '<table><tbody><tr>' + html + '</tr></tbody></table>';
        p = p.childNodes[0].childNodes[0].childNodes[0];
      } else {
        p.innerHTML = html;
      }
      return p;
    };

    // A simple inserter based on innerHTML.
    // Try this and if performance is slow
    // then look at the techniques used in Prototype
    // and keep this as a fallback.
    var insert = function(el, html, evalScripts, parser, inserter) {
      var ss = getScripts(html);
      html = stripScripts(html);

      inserter(el, parser(el, html));
    
      if (evalScripts === undefined || evalScripts === 'eval') {
        evalScripts(ss);
      }
    };

    var remove = function(el) {
      if (FORK.purgeElement) { // TODO should be using FORK('purgeElement') and double check prop name really is 'purgeElement'
        // Purge the element and it's descendents of all event handlers
        // added through FORK.Event.addListener() because IE leaks memory
        FORK.purgeElement(el, {deep:true});
      }
      el.parentNode.removeChild(el);
    };

    var insertBefore = function(el, html, evalScripts) {
      insert(el, html, evalScripts, outsideParser, 
             function(el, p) {
               var m,
                   n = p.firstChild;
               while (m = n) { // yes just one equals sign. saves a line of code
                 n = m.nextSibling;
                 el.parentNode.insertBefore(m, el);
               }
             });
    };

    var insertTop = function(el, html, evalScripts) {
      insert(el, html, evalScripts, insideParser,
             function(el, p) {
               var ns = p.childNodes;
               for (var i=ns.length; i--; ){
                 el.insertBefore(ns[i], el.firstChild);
               }
             });
    };
  
    var insertBottom = function(el, html, evalScripts) {
      insert(el, html, evalScripts, insideParser,
             function(el, p) {
               var m,
                   n = p.firstChild;
               while (m = n) { // yes just one equals sign. saves a line of code
                 n = m.nextSibling;
                 el.appendChild(m);
               }
             });
    };

    var insertAfter = function(el, html, evalScripts) {
      insert(el, html, evalScripts, outsideParser,
             function(el, p) {
               var ns = p.childNodes;
               for (var i=ns.length; i--; ){
                 el.parentNode.insertBefore(ns[i], el.nextSibling);
               }
             });
    };

    var empty = function(el) {
      for (var i=el.childNodes.length; i--; ) {
        remove(el.childNodes[i]);
      }
    };

    var update = function(el, html, evalScripts) {
      empty(el);
      insertBottom(el, html, evalScripts);
    };

    var replace = function(el, html, evalScripts) {
      var ss = getScripts(html);
      html = stripScripts(html);

      var parent = el.parentNode;
      var next = el.nextSibling;

      remove(el);

      // the following is very similar to insertBefore but because
      // "next" could be a text node cannot just call insertBefore and
      // have the parser work properly if this replace involves table elements
      var m,
          n = outsideParser(el,html).firstChild;
      while (m = n) { // Yes really just one equals sign. This saves a line of code.
         n = m.nextSibling;
         parent.insertBefore(m, next);
       }

       if (evalScripts === undefined || evalScripts === 'eval') {
         evalScripts(ss);
       }
    };
    
    // export
    FORK.remove       = remove;
    FORK.insertBefore = insertBefore;
    FORK.insertTop    = insertTop;
    FORK.insertBottom = insertBottom;
    FORK.insertAfter  = insertAfter
    FORK.empty        = empty;
    FORK.update       = update;
    FORK.replace      = replace;
    
  } // feature testing
  
})();

// event.js
(function() {

  var global = this;

  if (typeof FORK == 'function' &&
      FORK.isHostObject && // TODO need this check?
      FORK.isHostMethod &&
      typeof Function.prototype.apply == 'function' &&
      typeof Function.prototype.call == 'function' &&
      typeof Array.prototype.slice == 'function' &&
      typeof Array.prototype.push == 'function' &&
      (FORK.isHostMethod(global, 'addEventListener') || // TODO also need to test on an element. See Cross-browser widgets article.
       FORK.isHostMethod(global, 'attachEvent'))) {
      
    /**
     * Cache of listeners
     * @type array
     * @private
     */
    var listeners = [];

    /**
     * Cache of user-defined unload functions that will be fired
     * before all events are detached manually window.onunload
     * @type array
     * @private
     */
    var unloadListeners = [];
  
  
    // TODO
    var unloadListenerAttached;
  
    var useLegacyListener = function(type) {
      return (type === 'click' || type == 'dblclick'); // use === once here to get NN4 to syntax error asap
    };
  
  
    /**
     * Appends an event handler
     *
     * @param {Object}   el        The html element to assign the 
     *                             event to
     * @param {String}   type     The type of event to append
     * @param {Function} fn        The method the event invokes
     * @param {Object}   options   For example {scope:myObj, argument:otherObj}
     *                             The scope object will be the scope in which the handler
     *                             is called. The argument object (array, fnc, or primative etc) will be 
     *                             passed as a parameter to the handler.
     */
    var on = function(el, type, fn, options) {
      options = options || {};
    
      var obj = {el:el, type:type, fn:fn, options:options};
      // if the user chooses to override the scope, we use the custom
      // object passed in, otherwise the executing scope will be the
      // HTML element that the event is registered on
      var scope = (options.scope) ? options.scope : el;
    
      // wrap the function so it executes in the correct scope;
      if (options.args) {
          // shallow copy the arguments for the callback
          var args = Array.prototype.slice.call(options.args, 0);
          obj.wrappedFn = function(e) {
                            // prepend the arguments with the event object
                            args.unshift(e);
                            var r = fn.apply(scope, args);
                            // if the handler is called twice, don't want
                            // to have more and more events prepended so
                            // remove the event now
                            args.shift();
                            return r;
                          };
      } else {
          var argument = options.argument;
          obj.wrappedFn = function(e) {
                            return fn.call(scope, e, argument);
                          };
      }

    
      // we need to make sure we fire registered unload events 
      // prior to automatically unhooking them.  So we hang on to 
      // these instead of attaching them to the window and fire the
      // handlers explicitly during our one unload event.
      if ("unload" == type && unloadListenerAttached) {
        if (getCacheIndex(unloadListeners, el, type, fn) < 0) {
          unloadListeners.push(obj);
        }
        return;
      }


      var attached = false;
    
      if (useLegacyListener(type)) {
        if (!el['on' + type] ||
            !el['on' + type].legacyListeners) { // overwrites existing DOM0 event handler.
                                                // TODO write docs about this.

          el['on' + type] = function(e) {
                               e = e || window.event;
                               // This local variable that references the legacy listeners
                               // must not be named "legacyListeners" because
                               // Netscape 6 has a strange set of properties for
                               // arguments.callee that seems to combine all the properties
                               // of arguments.callee and the local variables.
                               //
                               // The use of slice to create a new array is in the case of a listener
                               // removing itself. Without the slice the next listener would not run
                               // as all the unrun listeners shift to an index one less. 
                               var lls = arguments.callee.legacyListeners.slice(0); // TODO remove use of callee (almost deprecated by ES committee)
                               for (var i=0, len=lls.length; i<len; i++) {
                                 var l = lls[i];
                                 if (l) {
                                   /*
                                   ** Wrap handler in a try-catch so that if one handler throws an error
                                   ** then the others still run. I tested IE6, FF1.5, O9, S2 and
                                   ** they all really do behave this way.
                                   */
                                   try {
                                     // When an event fires, if one listener removes a listener that has not yet run
                                     // then when the listener that has not yet run tries to run the next call to 
                                     // l.wrappedFn would error. This is because the listener that has not yet run has
                                     // had it's wrappedFn property set to null in the FORK.Event.removeListener().
                                     // Calling l.wrappedFn would throw a "wrappedFn is not a function" error.
                                     if (l.wrappedFn) {
                                         l.wrappedFn(e);
                                     }
                                   } catch (er) {
                                       // Throw the error but don't interupt calling other listeners
                                       setTimeout(function(){throw er;}, 0);
                                   }
                                 }
                               }
                             };

          el['on' + type].legacyListeners = [];

        } else if (getCacheIndex(el['on' + type].legacyListeners, el, type, fn) >= 0) {
          // already have this listener cached so we don't want to add it again
          return;
        }

        // cache with the handler, all legacyListeners for this element and event
        el['on' + type].legacyListeners.push(obj);
      
        attached = true;
      
      // DOM2 Event model
      } else if (el.addEventListener) {
          el.addEventListener(type, obj.wrappedFn, false);
          attached = true;
      // IE
      } else if (el.attachEvent) {
          el.attachEvent("on" + type, obj.wrappedFn);
          attached = true;
      }

      if (attached) {
        // cache all listeners
        listeners.push(obj);

        if ("unload" == type) {
          unloadListenerAttached = true;
        }
      }
    };
  
  
    /**
     * Removes an event handler
     *
     * @param {Object} el the html element or the id of the element to 
     * assign the event to.
     * @param {String} type the type of event to remove
     * @param {Function} fn the method the event invokes
     * @return {boolean} true if the unbind was successful, false 
     * otherwise
     */
    var off = function(el, type, fn) {

      var cache = (type=='unload' ? unloadListeners : listeners);

      var i = getCacheIndex(cache, el, type, fn);
      if (i < 0) {return;} // not found
      var obj = cache[i];
      cache.splice(i, 1);

      if (type != 'unload') {
        if (useLegacyListener(type)) {
          i = getCacheIndex(el['on' + type].legacyListeners, el, type, fn);
          el['on' + type].legacyListeners.splice(i, 1);
          if (el['on' + type].legacyListeners.length < 1) {
            el['on' + type] = null;
          }
        } else if (el.removeEventListener) {
          el.removeEventListener(type, obj.wrappedFn, false);
        } else if (el.detachEvent) {
          el.detachEvent("on" + type, obj.wrappedFn);
        }
      }
    
      // IE memory leak
      obj.fn = null;
      // TRICKY: Do not change the following line. See note about legacy listeners
      // in try-catch in FORK.Event.addListener()
      obj.wrappedFn = null;
    };
  
    /**
     * @private
     * Locating the saved event handler data by function ref
     */
    var getCacheIndex = function(arr, el, type, fn) {
      for (var i=arr.length; i--; ) {
        var li = arr[i];
        if ( li && li.el == el  && li.type == type && li.fn == fn ) {
          return i;
        }
      }
      return -1;
    };

    /**
     * Removes all listeners.  Called 
     * automatically during the unload event.
     * @private
     */
    var unload = function(e) {
      e = e || window.event;
      var i, l, len;
      for (i=0,len=unloadListeners.length; i<len; ++i) {
        l = unloadListeners[i];
        if (l) {
          try {
            l.wrappedFn(e);
          } catch (err) {}
          // IE memory leak
          l.fn = null;
          l.wrappedFn = null;
        }
      }
      for (i=listeners.length; i-- ; ) {
        var li = listeners[i];
        if (li) {
          FORK.off(li.el, li.type, li.fn);
        } 
      }
    };
  
  
    /**
     * Call purgeElement to remove all listeners added through the FORK.Event API
     * Always call this before removing an element from the DOM with the deep option true.
     * This method takes care of Internet Explorer memory leaks.
     *
     * @param {Object}   el        The html element to assign the 
     *                             event to
     * @param {Object}   options   For example {type:'click', deep:true}
     *                             The type specifies the type of event to purge. If omitted will purge all types.
     *                             If deep is true then all child elements will also be purged of
     *                             the same type or all depending on the type option.
     */
    var purgeElement = function(el, options) {
      options = options || {};
      var i,
          elListeners = getListeners(el, options.type);

      for (i=elListeners.length; i--; ) {
        var l = elListeners[i];
        off(el, l.type, l.fn);
      }
    
      if (options.deep && el.childNodes) {
        for (i=el.childNodes.length; i--; ) {
          this.purgeElement(el.childNodes[i], options);
        }
      }
    };

    /**
     * @private
     */
    var getListeners = function(el, type) {
      var elListeners = [];
      for (var i=listeners.length; i--;) {
        var l = listeners[i];
        if (l && l.el === el &&
            (!type || type === l.type) ) {
          elListeners.push(l);
        }
      }
      return elListeners;
    };
  
    /**
     * Stops event propagation
     * @param {Event} e the event
     */
    var stopPropagation = function(e) {
      if (e.stopPropagation) {
        e.stopPropagation();
        return true;
      }
      if (e.cancelBubble !== undefined) { // cancelBubble false by default
        e.cancelBubble = true;
        return true;
      }
      return false;
    };

    /**
     * Prevents the default behavior of the event
     * @param {Event} e the event
     */
    var preventDefault = function(e) {
      if (e.preventDefault) {
        e.preventDefault();
        return true;
      }
      if (e.cancelBubble !== undefined){ // can't test returnValue directly?
        e.returnValue = false;
        return true;
      }
      return false;
    };

    /**
     * Returns the event's target element
     * @param {Event} e the event
     * @return {HTMLElement} the event's target or null if one not found
     */
    var getTarget = function(e) {
      var t = e.target || e.srcElement;
      return resolveTextNode(t);
    };

    /**
     * In some cases, some browsers will return a text node inside
     * the actual element that was targeted.  This normalizes the
     * return value for getTarget and getRelatedTarget.
     * @param {HTMLElement} node The node to resolve
     * @return {HTMLElement} the normized node
     */
    var resolveTextNode = function(node) {
      if (node && node.nodeName && "#TEXT" == node.nodeName.toUpperCase()) {
        return node.parentNode;
      }
      return node;
    };

    /**
     * Returns the event's related target 
     * @param {Event} e the event
     * @return {HTMLElement} the event's relatedTarget or null if one not found
     */
    var getRelatedTarget = function(e) {
      var t = e.relatedTarget;
      if (!t) {
        if (e.type == "mouseout") {
          t = e.toElement;
        } else if (e.type == "mouseover") {
          t = e.fromElement;
        }
      }
      return resolveTextNode(t);
    };

  
    // setup IE memory clean up
    on(window, "unload", unload);   

    // export
    FORK.on = on;
    FORK.off = off;
    FORK.purgeElement = purgeElement; // TODO what should this be called and see mutate
    FORK.stopPropagation = stopPropagation;
    FORK.preventDefault = preventDefault;
    FORK.getTarget = getTarget;
    FORK.getRelatedTarget = getRelatedTarget;
    
  } // feature tests
  
})();

// xhr.js
var FORK = FORK || {};

FORK.Ajax = function(method, url, options) {
  
  this.setOptions(options);
  
  this.method = method.toUpperCase();

  this.request = FORK.Ajax.newXMLHttpRequest();
  if (!this.request) {return true;}

  // With Firefox 1.5 when abort() is called
  // a readystate 4 event is fired
  // which is a disaster. The "aborted" flag is a workaround
  // to avoid having callbacks fired when a request is aborted.
  // The Quirksmode website says that IE has this same bug and given
  // the date of writing it likely refers to IE 6.
  // http://www.quirksmode.org/blog/archives/2005/09/xmlhttp_notes_a_1.html
  this.aborted = false;

  // for handler closures
  var self = this;

  //this.timer;  
  if (this.options.timeout) {
    this.timer = setTimeout(function() {self.onTimeout();}, this.options.timeout);
  }

  this.request.onreadystatechange = function() {self.onReadyStateChange();};

  // begin prep the request body -------------------------------------------
  this.body = this.options.body || {};
  this.setMethod();

  // change the body to a string for sending to server
  this.body = (function(oBody) {
    var aBody = [];
    for (var p in oBody) {
      aBody.push(encodeURIComponent(p) + "=" + encodeURIComponent(oBody[p]));      
    }
    return ((aBody.length > 0) ? aBody.join("&") : null);
  })(this.body);

  var serialization = null;
  if (this.options.form) {
    serialization = FORK.Ajax.serializeForm(this.options.form);
  }

  if (this.body && serialization) {
    this.body = serialization + "&" + this.body;
  } else if (serialization) {
    this.body = serialization;
  }

  if (this.method === 'GET') {
    if (this.body) {
      url = url + ( url.match(/\?/) ? '&' : '?') + this.body;
    }
    this.body = null;
  }
  // end prep the body ------------------------------------------------------

  this.request.open(this.method, url, true);

  if (this.method === "POST") {
    this.request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  }

  if (this.options.headers) {
    for (p in this.options.headers) {
      this.request.setRequestHeader(p, this.options.headers[p]);
    }
  }
  
  this.request.send(this.body);
};

//FORK.Ajax.request = function(url, method, options) {
//  return new FORK.Ajax(url, method, options);
//};

// factored out for overriding in ajaxRails
FORK.Ajax.prototype.setOptions = function(options) {
  this.options = options || {};
};

// factored out for overriding in ajaxRails
FORK.Ajax.prototype.setMethod = function() {
  if (this.method === 'GET') {
    // Rumor has it that at least one browser caches GET requests
    // made with an XMLHttpRequest object or equivilant ActiveXObject.
    // (TODO find a reference supporting this rumor.)
    // To avoid using any cached response add a
    // unique dummy id to the URL to make it different than all previous
    // URLs and hence insure the request has never been cached.
    this.body._uniqueId = (new Date()).getTime() + "" + FORK.Ajax.transactionId++;
  }
};

FORK.Ajax.transactionId = 0;


FORK.Ajax.newXMLHttpRequest = function() {
  // Possible factories in reverse order of preference 
  // for use with efficient, reverse for-loop below.
  var fs = [
    function() { return new ActiveXObject("Microsoft.XMLHTTP"); },
    function() { return new ActiveXObject("Msxml2.XMLHTTP"); },
    function() { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); },
    function() { return new XMLHttpRequest(); }
  ];

  // Loop through the possible factories to try and find one that 
  // can instantiate an XMLHttpRequest object.
  for (var i=fs.length; i--; ) {
    try {
      // try to instantiate an XMLHttpRequest object
      var r = fs[i]();
      if (r) {
        FORK.Ajax.newXMLHttpRequest = fs[i];
        return r;
      }
    } catch (e) {}
  }

  (FORK.Ajax.newXMLHttpRequest = function() {return null;})();
};

// f the form element
FORK.Ajax.serializeForm = function(f) {
  if (typeof f == 'string') {
    // If f is a string then it can be the form's id or name attribute
    // Could be that only the forms collection is needed because it can find by id also 
    f = document.getElementById(f) || document.forms[f];
  }

  var els = f.elements,
      cereal = []; // the serialization of the form data into a string

  function add(n, v) { 
    cereal.push(encodeURIComponent(n) + "=" + encodeURIComponent(v));
  }

  for (var i=0, ilen=els.length; i<ilen; i++) {
    var el = els[i];
    if (!el.disabled) {
      switch (el.type) {
        case 'text': case 'password': case 'hidden': case 'textarea':
          add(el.name, el.value);
          break;
        case 'select-one':
          if (el.selectedIndex >= 0) {
            add(el.name, el.options[el.selectedIndex].value);
          }
          break;
        case 'select-multiple':
          for (var j=0, jlen=el.options.length; j<jlen; j++) {
            var opt = el.options[j];
            if (opt.selected) {
              add(el.name, opt.value);
            }
          }
          break;
        case 'checkbox': case 'radio':
          if (el.checked) {
            add(el.name, el.value);
          }
          break;
      }
    }
  }
  if (this.button) {
    add(this.button.name, this.button.value);
    this.button = null;
  }
  return ((cereal.length > 0) ? cereal.join("&") : null);
};


FORK.Ajax.setButton = function(el) {
  this.button = {name:el.name, value:el.value};
};


FORK.Ajax.prototype.doCallback = function(sMethod) {
  if (this.options.scope) {
    // If a scope property is defined, the callback will be fired from
    // the context of the object.
    this.options[sMethod].call(this.options.scope, this.request, this.options.argument);
  } else {
    this.options[sMethod](this.request, this.options.argument);
  }
};

FORK.Ajax.prototype.onReadyStateChange = function() {
  if (!this.aborted && this.request.readyState === 4) {
    if (this.timer) {clearTimeout(this.timer);}
    if (this.request) { // why this conditional?
      this.handleReadyState4();
    }
    // Break circle for IE memory leak
    // Richard Cornford wrote a nice description handling the IE memory leak problem:
    //
    // "There are two good candidates for breaking the circle; the "request"
    // property of the Activation/Variable object refers to the xml http request
    // object, so could have - null - assigned to it after the callback function
    // processes the response, and - onreadystatechange - property of the xml
    // http request object refers to the anonymous function object, and could be
    // re-assigned. Unfortunately assigning a non-function to the -
    // onreadystatechange - property does not work in IE (an allowable quirk of
    // some ActiveX objects), so the value assigned to break the circle would
    // have to be a function, but that function could be a simple (and
    // re-useable) dummy that just did not have a scope chain that held any
    // references to any xml http request obejcts."
    //
    // The original post:
    // http://groups.google.com/group/comp.lang.javascript/msg/556048483801bd96
    //
    // Here I choose to use the empty function approach because if the developer
    // want's to hang on to the object returned by "new FORK.Ajax()" then
    // the request property will still point to the request object and the developer
    // can manage the memory leak himself. 
    //
    //this.request = null;
    this.request.onreadystatechange = FORK.Ajax.emptyFnc;
  }
  
};

FORK.Ajax.emptyFnc = function(){};

FORK.Ajax.prototype.handleReadyState4 = function() {
  var request = this.request,
      options = this.options;

  var status; // holds the request status
  
  // This try-catch block is taken from YUI connection library
  // which doesn't state under which conditions "request.status"
  // will throw an error. More research about this is required
  // and perhaps the try-catch can be removed with better
  // understanding. However having the try-catch probably doesn't
  // cause any problem.
  try {
    status = request.status;
  } catch(e) {
    // 13030 is the custom code (invented by YUI?) to indicate
    // the condition -- in Mozilla/FF --
    // when the request object's status property is
    // unavailable and a query attempt throws an exception.
    status = 13030;
  }

  // If the request status indicates failure then we make a special
  // object to be passed to the callback handlers as though this object
  // is the request object. The following status value, except the last,
  // are wininet.dll error codes.
  if (status == 12002 || // Server timeout
      status == 12029 || // dropped connections
      status == 12030 || // dropped connections
      status == 12031 || // dropped connections
      status == 12152 || // Connection closed by server.
      status == 13030) { // See above comments for variable status.
    this.request = {status: 0,
                    statusText: "communication failure",
                    argument: options.argument};
  }

  // execute the callbacks -----------------------------------------------
  
  if (options.before) {
    this.doCallback("before");
  }

  this.status = status;
  this.middleCallback();

  if (options.after) {
    this.doCallback("after");
  }
}; // handleReadyState4()

// factored out for overriding in ajaxRails
FORK.Ajax.prototype.middleCallback = function() {
  if (this.options["on"+this.status]) {
    this.doCallback("on"+this.status);
  } else if (this.status >= 200 && this.status < 300 && this.options.onSuccess) {
    this.doCallback("onSuccess");
  } else if ((this.status < 200 || this.status >= 300) && this.options.onFailure) {
    this.doCallback("onFailure"); 
  } else if (this.options.onComplete) {
    this.doCallback("onComplete");
  } 
};

FORK.Ajax.prototype.abort = function() {
  this.aborted = true;
  this.request.abort();
  // Break circle for IE memory leak
  // see notes above
  //this.request = null;
  request.onreadystatechange = FORK.Ajax.emptyFnc;
};


FORK.Ajax.prototype.onTimeout = function() {
  this.aborted = true;
  this.request.abort();
  this.handleTimeout();
  // Break circle for IE memory leak
  // see notes below
  this.request = null;
  //request.onreadystatechange = FORK.Ajax.emptyFnc;
};

FORK.Ajax.prototype.handleTimeout = function() {
  if (this.options.before) {
    this.doCallback("before");
  }
  if (this.options.onTimeout) {
    this.doCallback("onTimeout");
  }
  if (this.options.after) {
    this.doCallback("after");
  }
};


FORK.Ajax.isSupported = (function(){
  var en = false,
      x;

  // NN6.1 can create an XMLHttpRequest object but it doesn't function
  // and after creation the object's readyState property is 'undefined'
  // when it should be '0'. The XHR object in NN6.1 doesn't work.
  
  try {
    if (typeof (function(){}).call === "function" &&
        (x = FORK.Ajax.newXMLHttpRequest()) && // yes just one equals sign
        x.readyState === 0) {
      en = true;
    }
  } catch(e) {
    en = false;
  }

  // IE 5.01sp2 and errors when x.setRequestHeader runs
  // The error says "object doesn't support this property"
  // I think this is because xhr.open() hasn't been called yet
  //
  // Opera 8 XHR doesn't have setRequestHeader() and although it seems
  // to be able to make a POST request with a body without this function
  // This browser is still going to be put down the degredation path
  // Note Opera 8.0.1 does have setRequestHeader() and can POST
  try {
    if (!x.setRequestHeader) {
      en = false;
    }
  } catch(e) {}


  // Right here NN6.2 will report that "en" is true but 
  // NN6.2 can't make POST requests because can't have arguments to send()
  // so now catch NN6.2 and any other browsers that can't take argument to XHR.send()
  function cannotPost() {
    var xhr = new XMLHttpRequest();
    try {
      xhr.send("asdf");
    } catch (e) {
      // All calls to xhr.send() should error because there wasn't a call to xhr.open()
      // however the normal error is something about "not initialized" as expected
      // since xhr.open() was not called. NN6.2 gives a different error indicating
      // xhr.send() cannot take arguments.
      if (-1 !== e.toString().indexOf("Could not convert JavaScript argument arg 0 [nsIXMLHttpRequest.send]")) {
        return true;
      }
    }
    return false;
  }
  if (this.XMLHttpRequest && cannotPost()) {
    en = false;
  }
  
  return function(){return en;};
})();

FORK.xhr = function(url, method, options) {
  return new FORK.Ajax(url, method, options);
};

// find.js
(function() {
  
  if (typeof FORK == 'function' &&
      FORK.isHostMethod &&
      typeof Sly == 'function' &&
      FORK.isHostMethod(Sly, "search")) {
      
    FORK.find = function(selector, rootEl) {
      return Sly.search(selector, rootEl);
    };

  } // feature test

})();

