/*

HTMLHttpRequest v1.0RC1 Preview
(c) 2001-2007 Angus Turnbull, TwinHelix Designs http://www.twinhelix.com

Licensed under the CC-GNU LGPL, version 2.1 or later:
http://creativecommons.org/licenses/LGPL/2.1/
This is distributed WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

*/



// Some common event API code I use. Syntax:
//   addEvent(object_reference, 'name_of_event', function_reference, legacy);
// 'name_of_event' is without the 'on' prefix, e.g. 'load', 'click' or 'mouseover'.
// 'legacy' disables addEventListener if true.
// You can add multiple events to one object, and in MSIE all are removed onunload.

if (typeof addEvent != 'function')
{
 var addEvent = function(o, t, f, l)
 {
  var d = 'addEventListener', n = 'on' + t;
  if (o[d] && !l) return o[d](t, f, false);
  if (!o._evts) o._evts = {};
  if (!o._evts[t])
  {
   o._evts[t] = {};
   if (o[n]) addEvent(o, t, o[n], l);
   o[n] = new Function('e',
    'var r = true, o = this, a = o._evts["' + t + '"], i; for (i in a) {' +
    'o._f = a[i]; if (o._f._i) r = o._f(e||window.event) != false && r;' +
    '} o._f = null; return r');
  }
  if (!f._i) f._i = addEvent._i++;
  o._evts[t][f._i] = f;
  if (t != 'unload') addEvent(window, 'unload', function() {
   removeEvent(o, t, f, l);
  });
 };
 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();
 }
};



var HTMLHttp = {};

// SYNTAX INSTRUCTIONS for HTMLHttp.Request:
//
// var objectName = new HTMLHttp.Request('objectName');
//
// Create an instance of an HTMLHttp object, and pass its own name as a string.
// NOTE: All requested documents must reside on the same domain as this document!
//
// Available methods are:
//
// objectName.load('file.html', callback_function);
//
// This will issue a GET request to the server to return a named file.
// Returns true if the load is successful.
//
// objectName.submit(form_reference, callback_function);
//
// This will capture a form's submission and redirect it to a background POST or GET
// request, respecting the form's 'method' attribute and its 'action' URI.
// Pass it a reference to the form's DOM node.
// Note that the form should already be in the process of submission when this is called.
// It is therefore suggested that you call it from within an ONSUBMIT handler on a form.
// It will return "true" when the capture is successful and the form submittal may
// be cancelled, or "false" when either the browser is unsupported or the IFRAME
// submittal method is used (in which case the form submit should proceed).
//
// You can/should also pass callback functions as second parameters above.
// They will be called on load/submit as methods of this object with parameters:
//  1) A reference to the loaded DOM document (which you may then parse).
//  2) The text content of the loaded document.

HTMLHttp.Request = function(myName)
{
 this.myName = myName;
 this.callback = null;
 // A loading flag, set to the requested URI when loading.
 this.loadingURI = '';

 // 'xmlhttp': Our preferred request object.
 this.xmlhttp = null;

 // Attempt to init an XMLHttpRequest object where supported.
 // Note: MSIE7 is best with the IFRAME method, so exclude IE here.
 if (window.XMLHttpRequest && !window.ActiveXObject) this.xmlhttp = new XMLHttpRequest();

 if (!this.xmlhttp && window.ActiveXObject)
 {
  try
  {
   this.xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
  }
  catch (e)
  {
   try
   {
    this.xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
   }
   catch (e) { }
  }
 }

 // If installed, activate the IFRAME buffer setup.
 // Not required in new browsers, shipped in separate .JS file.
 if (!this.xmlhttp && this.iframeSetup) this.iframeSetup();

 // TODO: Put in further DOM3 LSParser support as a transport layer...?

 return this;
};



HTMLHttp.Request.prototype.parseForm = function(form)
{
 // Parses a form DOM reference to an escaped string suitable for GET/POSTing.

 var str = '', gE = 'getElementsByTagName', inputs = [
  (form[gE] ? form[gE]('input') : form.all ? form.all.tags('input') : []),
  (form[gE] ? form[gE]('select') : form.all ? form.all.tags('select') : []),
  (form[gE] ? form[gE]('textarea') : form.all ? form.all.tags('textarea') : [])
 ];

 // Loop through each list of tags, constructing our string.
 for (var i = 0; i < inputs.length; i++)
  for (j = 0; j < inputs[i].length; j++)
   if (inputs[i][j])
   {
    var elm = inputs[i][j];
    if ((elm.nodeName.toLowerCase() == 'input') &&
        { 'radio': 1, 'checkbox': 1 }[elm.type] && !elm.checked) continue;
    var plus = '++'.substring(0,1); // CodeTrim fix. Yeah, I know.
    str += escape(elm.name).replace(plus, '%2B') +
     '=' + escape(elm.value).replace(plus, '%2B') + '&';
   }

 // Strip trailing ampersand, because we can :)
 return str.substring(0, str.length - 1);
};



HTMLHttp.Request.prototype.xmlhttpSend = function(uri, formStr)
{
 // Use XMLHttpRequest to asynchronously open a URI, and optionally POST a provided
 // form string if any (otherwise, performs a GET).

 this.xmlhttp.open(formStr ? 'POST' : 'GET', uri, true);
 var obj = this;
 this.xmlhttp.onreadystatechange = function()
 {
  if (obj.xmlhttp.readyState == 4)
  {
   // If you are getting an error where either of these value are null, try changing
   // the MIME type returned by the server: setting it to text/xml usually works well!
   if (obj.callback) obj.callback(obj.xmlhttp.responseXML, obj.xmlhttp.responseText);
   obj.loadingURI = '';
  }
 };
 if (formStr)
  this.xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
 // You might need to customise this.
 if (typeof this.xmlhttp.overrideMimeType == 'function')
  this.xmlhttp.overrideMimeType((/\.txt/i).test(uri) ? 'text/plain' : 'text/xml');

 this.xmlhttp.send(formStr);
 this.loadingURI = uri;
 return true;
};



// *** PUBLIC METHODS ***

HTMLHttp.Request.prototype.load = function(uri, callback)
{
 // Pass a URI and a callback function to load a plain text document.
 // Returns true if the load was successful.

 if (!uri || (!this.xmlhttp && !this.iframe)) return false;
 // Route the GET through an available transport layer.
 this.callback = callback;
 if (this.xmlhttp) return this.xmlhttpSend(uri, '');
 else if (this.iframe) return this.iframeSend(uri, null);
 else return false;
};


HTMLHttp.Request.prototype.submit = function(formRef, callback)
{
 // Pass a reference to a (submitting) form DOM node and a callback function.
 // Returns true if the load/capture was successful.

 if (!formRef || (!this.xmlhttp && !this.iframe)) return false;

 // Retrieve form information then decide what to do with it.
 var method = formRef.getAttribute('method'), uri = formRef.getAttribute('action');

 if (method && method.toUpperCase() == 'POST')
 {
  // Send the URI and either a parsed form or a form reference to the transports.
  // Note we only cancel form for XMLHTTP, as IFRAMEs still need it to submit.
  this.callback = callback;
  if (this.xmlhttp) return this.xmlhttpSend(uri, this.parseForm(formRef));
  else if (this.iframe) return this.iframeSend(uri, formRef);
  else return false;
 }
 else
 {
  // For GET requests, append ?querystring or &querystring to the GET uri and
  // forward it to the load() function (along with the callback).
  return this.load(uri + (uri.indexOf('?') == -1 ? '?' : '&') + this.parseForm(formRef),
   callback);
 }
};












// SYNTAX INSTRUCTIONS for HTMLHttp.Threader:
//
// var objectName = new HTMLHttp.Threader('objectName');
//
// This is our "Threading" layer that automatically creates, allocates and processes
// HTMLHttp.Request objects, so you can easily issue concurrent requests to a server
// and load areas of content into your page without writing complex management code.
// Pass its own name in quotes.
//
// Available public methods and properties are:
//
// objectName.loadInto('file.html', 'id-of-target', callback_function);
//
// This loads a specified file into an element with an ID="id-of-target" attribute.
//
// objectName.submitInto(form_reference, 'id-of-target', callback_function);
//
// This will capture a form's submission and load within the specified target element.
// If you are using the IFRAME transport plugin, the form should already be in the
// process of submittal, otherwise it can be called at any time.
//
// Callback functions for both the above receive the following parameters:
// 1) The loaded document DOM.
// 2) The loaded document text.
// 3) The destination ID.
//
// Note that for both loadInto() and submitInto(), subsequent requests targeted at the
// same element will cancel earlier requests if they are still loading.
// Also, all loaded files must reside on the same domain as the requesting document.

HTMLHttp.Threader = function(myName)
{
 this.myName = myName;
 this.threads = [];
 this.loadingIDs = {};
};


HTMLHttp.Threader.prototype.getThread = function(destId, callback) { with (this)
{
 // Locates a thread that's EITHER: loading into the specified destination, OR: idle.
 // If none are match that condition, create a new one.

 var thr = -1;

 for (var id in loadingIDs)
 {
  // Same destination?
  if (id == destId)
  {
   thr = loadingIDs[id];
   break;
  }
 }
 if (thr == -1) for (var t = 0; t < threads.length; t++)
 {
  // Idle?
  if (!threads[t].loadingURI)
  {
   thr = t;
   break;
  }
 }
 if (thr == -1)
 {
  // Create a new HTMLHttp.Request object.
  thr = threads.length;
  threads[thr] = new HTMLHttp.Request(myName + '.threads[' + thr + ']');
  // Record this thread as loading for this destination, so it can be aborted/reused.
  loadingIDs[destId] = thr;
 }

 return threads[thr];
}};


HTMLHttp.Threader.prototype.loadInto = function(uri, destId, callback)
{
 // Pass the file onto the load method of a selected/new thread.
 var threader = this;
 var cbFunc = function(docDOM, docText)
 {
  delete threader.loadingIDs[destId];
  callback(docDOM, docText, destId);
 };
 return this.getThread(destId).load(uri, cbFunc);
};


HTMLHttp.Threader.prototype.submitInto = function(formRef, destId, callback)
{
 // Pass the file onto the submit method of the selected thread.
 var threader = this;
 var cbFunc = function(docDOM, docText)
 {
  delete threader.loadingIDs[destId];
  callback(docDOM, docText, destId);
 };
 return this.getThread(destId).submit(formRef, cbFunc);
};





// SYNTAX INSTRUCTIONS for HTMLHttp.Capture:
//
// var objectName = new HTMLHttp.Capture('objectName');
//
// Available methods:
//
// objectName.apply(DOMNode);
//
// This will activate all click capturing within the supplied document/node.
// The below functions are then automatically called for all elements with the
// appropriate CLASS attributes, no further scripting required.
//
// objectName.loadInto(uri, destId, event, callback_function)
//
// Takes a URI and a destination ID (compulsory) and optionally an event object
// (used with active form submittal) or callback function reference (see below).
// Will then GET the specified document and insert into the given target.
//
// objectName.toggleInto(...)
//
// The same as loadInto but will collapse/expand target on subsequent clicks.
//
// objectName.submitInto(formRef, destId, event, callback_function)
//
// Called during a form submission with a reference to the form and other
// parameters as per loadInto().
//
// If you specify a custom callback function, it will be called with parameters:
// 1) Reference to loaded DOM document.
// 2) Loaded text/html document content.
// 3) The destination ID into which content should be inserted.
//
// The copyContent function is the "default" callback and takes those parameters.
// If you specify a custom callback, you must call copyContent or an equivalent
// function yourself.

HTMLHttp.Capture = function(myName, waitHTML)
{
 this.myName = myName;
 this.threader = new HTMLHttp.Threader(myName + '.threader');
 this.waitHTML = waitHTML;
};


HTMLHttp.Capture.prototype.loadInto = function(uri, destId, evt, callback)
{
 // Called to when a link with class="loadinto-IdOfTarget" is clicked.
 // Parameters: uri = address to load, destId = ID of target element,
 // evt = event object, callback = [optional] function reference.
 if (this.waitHTML) document.getElementById(destId).innerHTML = this.waitHTML;
 var ok = this.threader.loadInto(uri, destId, (callback || this.copyContent));
 if (ok && evt) cancelEvent(evt);
};


HTMLHttp.Capture.prototype.toggleInto = function(uri, destId, evt, callback)
{
 // As above, but loads only once and toggles the display of the target.
 var dest = document.getElementById(destId);
 if (!dest.contentLoaded)
  if (this.loadInto(uri, destId, evt, callback))
   dest.contentLoaded = true;
 if (evt) cancelEvent(evt);

 var src = evt.srcElement || evt.target;
 if (src.nodeType && src.nodeType != 1) src = src.parentNode;
 if (!dest.toggleState)
 {
  src.innerHTML = 'Close: ' + src.innerHTML;
  dest.style.display = 'block';
  // Add some animation in here if you want!
  dest.toggleState = 1;
 }
 else
 {
  src.innerHTML = src.innerHTML.replace(/^Close: /, '');
  dest.style.display = 'none';
  dest.toggleState = 0;
 }
};


HTMLHttp.Capture.prototype.submitInto = function(formRef, destId, evt, callback)
{
 // Called to when a link with class="submitinto-IdOfTarget" is clicked.
 // Parameters: src = reference to link, destId = ID of target element, evt = event object.
 if (this.waitHTML) document.getElementById(destId).innerHTML = this.waitHTML;
 if (this.threader.submitInto(formRef, destId, (callback || this.copyContent)))
  cancelEvent(evt);
};


HTMLHttp.Capture.prototype.apply = function(node)
{
 var obj = this;
 addEvent(node, 'click', function(evt)
 {
  // Here we capture all clicks on the document, scanning for links with a CLASS
  // attribute of "(load|toggle|submit)into-IdOfTarget" and processing them.
  evt = evt || window.event;
  // Only process left clicks.
  if (evt.which > 1 || evt.button > 1) return;
  var src = evt.target || evt.srcElement;
  if (src.nodeType && src.nodeType != 1) src = src.parentNode;
  var submitBtn = false;
  // Loop up the DOM tree scanning all elements to find a matching one.
  while (src)
  {
   var srcName = (src.nodeName||src.tagName||'').toLowerCase();
   if (srcName == 'input' && src.type.toLowerCase() == 'submit') submitBtn = true;
   if (srcName == 'form' && submitBtn &&
       src.className && src.className.match(/^submitinto-(.+)$/))
   {
    return obj.submitInto(src, RegExp.$1, evt, null);
   }
   if (srcName == 'a' && src.className && src.className.match(/^(load|toggle)into-(.+)$/))
   {
    var uri = src.href || src.getAttribute('href');
    // Call our load handlers if we have a match; they'll cancel the normal action.
    if (RegExp.$1 == 'load') return obj.loadInto(uri, RegExp.$2, evt, null);
    if (RegExp.$1 == 'toggle') return obj.toggleInto(uri, RegExp.$2, evt, null);
   }
   src = src.parentNode;
  }
 });
};


HTMLHttp.Capture.prototype.copyContent = function(docDOM, docText, destId)
{
 // This copies the <body> content of the loaded DOM document into an element in the
 // current page with a specified ID. It's suggested that you call it manually
 // from within your ONLOAD function if you're doing it yourself.

 // Retrieve references to the loaded BODY. You might want to modify this so that you
 // load content from <div id="content"> within the loaded document perhaps?
 // Note: Often fails in MSIE using XMLHTTP transport (fallbacks below).
 var src = docDOM ?
  (typeof docDOM.getElementsByTagName == 'function' ?
   docDOM.getElementsByTagName('body').item(0) :
    (docDOM.body ? docDOM.body : null) ) :
     null;
 var dest = document.getElementById ? document.getElementById(destId) :
  (document.all ? document.all[destId] : null);
 if (!dest || (!src && !docText)) return;

 // innerHTML is still a little more reliable than importNode across browsers.
 if (src && src.innerHTML) dest.innerHTML = src.innerHTML;
 else if (src && document.importNode)
 {
  while (dest.firstChild) dest.removeChild(dest.firstChild);
  for (var i = 0; i < src.childNodes.length; i++)
   dest.appendChild(document.importNode(src.childNodes.item(i), true));
 }
 else if (docText)
 {
  if (docText.match(/(<body>)(.*)(<\/body>)/i)) docText = RegExp.$2;
  // You might want to do some post-processing here if you are rendering
  // plain text in an XHTML document, for instance to keep line breaks:
  //docText = '<pre>' + docText + '</pre>';
  dest.innerHTML = docText;
 }
};

