private function for handling file uploads (hat tip to YAHOO!)
function fileUpload() {
    var form = $form[0];

    if ($(':input[name=submit],:input[id=submit]', form).length) {
      // if there is an input with a name or id of 'submit' then we won't be
      // able to invoke the submit fn on the form (at least not x-browser)
      alert('Error: Form elements must not have name or id of "submit".');
      return;
    }
    
    var s = $.extend(true, {}, $.ajaxSettings, options);
    s.context = s.context || s;
    var id = 'jqFormIO' + (new Date().getTime()), fn = '_'+id;
    window[fn] = function() {
      var f = $io.data('form-plugin-onload');
      if (f) {
        f();
        window[fn] = undefined;
        try { delete window[fn]; } catch(e){}
      }
    }
    var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ s.iframeSrc +'" onload="window[\'_\'+this.id]()" />');
    var io = $io[0];

    $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });

    var xhr = { // mock object
      aborted: 0,
      responseText: null,
      responseXML: null,
      status: 0,
      statusText: 'n/a',
      getAllResponseHeaders: function() {},
      getResponseHeader: function() {},
      setRequestHeader: function() {},
      abort: function() {
        this.aborted = 1;
        $io.attr('src', s.iframeSrc); // abort op in progress
      }
    };

    var g = s.global;
    // trigger ajax global events so that activity/block indicators work like normal
    if (g && ! $.active++) {
      $.event.trigger("ajaxStart");
    }
    if (g) {
      $.event.trigger("ajaxSend", [xhr, s]);
    }

    if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
      if (s.global) { 
        $.active--;
      }
      return;
    }
    if (xhr.aborted) {
      return;
    }

    var cbInvoked = false;
    var timedOut = 0;

    // add submitting element to data if we know it
    var sub = form.clk;
    if (sub) {
      var n = sub.name;
      if (n && !sub.disabled) {
        s.extraData = s.extraData || {};
        s.extraData[n] = sub.value;
        if (sub.type == "image") {
          s.extraData[n+'.x'] = form.clk_x;
          s.extraData[n+'.y'] = form.clk_y;
        }
      }
    }

    // take a breath so that pending repaints get some cpu time before the upload starts
    function doSubmit() {
      // make sure form attrs are set
      var t = $form.attr('target'), a = $form.attr('action');

      // update form attrs in IE friendly way
      form.setAttribute('target',id);
      if (form.getAttribute('method') != 'POST') {
        form.setAttribute('method', 'POST');
      }
      if (form.getAttribute('action') != s.url) {
        form.setAttribute('action', s.url);
      }

      // ie borks in some cases when setting encoding
      if (! s.skipEncodingOverride) {
        $form.attr({
          encoding: 'multipart/form-data',
          enctype:  'multipart/form-data'
        });
      }

      // support timout
      if (s.timeout) {
        setTimeout(function() { timedOut = true; cb(); }, s.timeout);
      }

      // add "extra" data to form if provided in options
      var extraInputs = [];
      try {
        if (s.extraData) {
          for (var n in s.extraData) {
            extraInputs.push(
              $('<input type="hidden" name="'+n+'" value="'+s.extraData[n]+'" />')
                .appendTo(form)[0]);
          }
        }

        // add iframe to doc and submit the form
        $io.appendTo('body');
        $io.data('form-plugin-onload', cb);
        form.submit();
      }
      finally {
        // reset attrs and remove "extra" input elements
        form.setAttribute('action',a);
        if(t) {
          form.setAttribute('target', t);
        } else {
          $form.removeAttr('target');
        }
        $(extraInputs).remove();
      }
    }

    if (s.forceSync) {
      doSubmit();
    }
    else {
      setTimeout(doSubmit, 10); // this lets dom updates render
    }
  
    var data, doc, domCheckCount = 50;

    function cb() {
      if (cbInvoked) {
        return;
      }

      $io.removeData('form-plugin-onload');
      
      var ok = true;
      try {
        if (timedOut) {
          throw 'timeout';
        }
        // extract the server response from the iframe
        doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
        
        var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
        log('isXml='+isXml);
        if (!isXml && window.opera && (doc.body == null || doc.body.innerHTML == '')) {
          if (--domCheckCount) {
            // in some browsers (Opera) the iframe DOM is not always traversable when
            // the onload callback fires, so we loop a bit to accommodate
            log('requeing onLoad callback, DOM not available');
            setTimeout(cb, 250);
            return;
          }
          // let this fall through because server response could be an empty document
          //log('Could not access iframe DOM after mutiple tries.');
          //throw 'DOMException: not available';
        }

        //log('response detected');
        cbInvoked = true;
        xhr.responseText = doc.documentElement ? doc.documentElement.innerHTML : null; 
        xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
        xhr.getResponseHeader = function(header){
          var headers = {'content-type': s.dataType};
          return headers[header];
        };

        var scr = /(json|script)/.test(s.dataType);
        if (scr || s.textarea) {
          // see if user embedded response in textarea
          var ta = doc.getElementsByTagName('textarea')[0];
          if (ta) {
            xhr.responseText = ta.value;
          }
          else if (scr) {
            // account for browsers injecting pre around json response
            var pre = doc.getElementsByTagName('pre')[0];
            if (pre) {
              xhr.responseText = pre.innerHTML;
            }
          }        
        }
        else if (s.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
          xhr.responseXML = toXml(xhr.responseText);
        }
        data = $.httpData(xhr, s.dataType);
      }
      catch(e){
        log('error caught:',e);
        ok = false;
        xhr.error = e;
        $.handleError(s, xhr, 'error', e);
      }

      // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
      if (ok) {
        s.success.call(s.context, data, 'success', xhr);
        if (g) {
          $.event.trigger("ajaxSuccess", [xhr, s]);
        }
      }
      if (g) {
        $.event.trigger("ajaxComplete", [xhr, s]);
      }
      if (g && ! --$.active) {
        $.event.trigger("ajaxStop");
      }
      if (s.complete) {
        s.complete.call(s.context, xhr, ok ? 'success' : 'error');
      }

      // clean up
      setTimeout(function() {
        $io.removeData('form-plugin-onload');
        $io.remove();
        xhr.responseXML = null;
      }, 100);
    }

    function toXml(s, doc) {
      if (window.ActiveXObject) {
        doc = new ActiveXObject('Microsoft.XMLDOM');
        doc.async = 'false';
        doc.loadXML(s);
      }
      else {
        doc = (new DOMParser()).parseFromString(s, 'text/xml');
      }
      return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
    }
  }