--- /dev/null
+/**
+ * JsHttpRequest: JavaScript "AJAX" data loader
+ *
+ * @license LGPL
+ * @author Dmitry Koterov, http://en.dklab.ru/lib/JsHttpRequest/
+ * @version 5.x $Id$
+ */
+
+// {{{
+function JsHttpRequest() {
+ // Standard properties.
+ var t = this;
+ t.onreadystatechange = null;
+ t.readyState = 0;
+ t.responseText = null;
+ t.responseXML = null;
+ t.status = 200;
+ t.statusText = "OK";
+ // JavaScript response array/hash
+ t.responseJS = null;
+
+ // Additional properties.
+ t.caching = false; // need to use caching?
+ t.loader = null; // loader to use ('form', 'script', 'xml'; null - autodetect)
+ t.session_name = "PHPSESSID"; // set to SID cookie or GET parameter name
+
+ // Internals.
+ t._ldObj = null; // used loader object
+ t._reqHeaders = []; // collected request headers
+ t._openArgs = null; // parameters from open()
+ t._errors = {
+ inv_form_el: 'Invalid FORM element detected: name=%, tag=%',
+ must_be_single_el: 'If used, <form> must be a single HTML element in the list.',
+ js_invalid: 'JavaScript code generated by backend is invalid!\n%',
+ url_too_long: 'Cannot use so long query with GET request (URL is larger than % bytes)',
+ unk_loader: 'Unknown loader: %',
+ no_loaders: 'No loaders registered at all, please check JsHttpRequest.LOADERS array',
+ no_loader_matched: 'Cannot find a loader which may process the request. Notices are:\n%'
+ }
+
+ /**
+ * Aborts the request. Behaviour of this function for onreadystatechange()
+ * is identical to IE (most universal and common case). E.g., readyState -> 4
+ * on abort() after send().
+ */
+ t.abort = function() { with (this) {
+ if (_ldObj && _ldObj.abort) _ldObj.abort();
+ _cleanup();
+ if (readyState == 0) {
+ // start->abort: no change of readyState (IE behaviour)
+ return;
+ }
+ if (readyState == 1 && !_ldObj) {
+ // open->abort: no onreadystatechange call, but change readyState to 0 (IE).
+ // send->abort: change state to 4 (_ldObj is not null when send() is called)
+ readyState = 0;
+ return;
+ }
+ _changeReadyState(4, true); // 4 in IE & FF on abort() call; Opera does not change to 4.
+ }}
+
+ /**
+ * Prepares the object for data loading.
+ * You may also pass URLs like "GET url" or "script.GET url".
+ */
+ t.open = function(method, url, asyncFlag, username, password) { with (this) {
+ // Extract methor and loader from the URL (if present).
+ if (url.match(/^((\w+)\.)?(GET|POST)\s+(.*)/i)) {
+ this.loader = RegExp.$2? RegExp.$2 : null;
+ method = RegExp.$3;
+ url = RegExp.$4;
+ }
+ // Append SID to original URL. Use try...catch for security problems.
+ try {
+ if (
+ document.location.search.match(new RegExp('[&?]' + session_name + '=([^&?]*)'))
+ || document.cookie.match(new RegExp('(?:;|^)\\s*' + session_name + '=([^;]*)'))
+ ) {
+ url += (url.indexOf('?') >= 0? '&' : '?') + session_name + "=" + this.escape(RegExp.$1);
+ }
+ } catch (e) {}
+ // Store open arguments to hash.
+ _openArgs = {
+ method: (method || '').toUpperCase(),
+ url: url,
+ asyncFlag: asyncFlag,
+ username: username != null? username : '',
+ password: password != null? password : ''
+ }
+ _ldObj = null;
+ _changeReadyState(1, true); // compatibility with XMLHttpRequest
+ return true;
+ }}
+
+ /**
+ * Sends a request to a server.
+ */
+ t.send = function(content) {
+ if (!this.readyState) {
+ // send without open or after abort: no action (IE behaviour).
+ return;
+ }
+ this._changeReadyState(1, true); // compatibility with XMLHttpRequest
+ this._ldObj = null;
+
+ // Prepare to build QUERY_STRING from query hash.
+ var queryText = [];
+ var queryElem = [];
+ if (!this._hash2query(content, null, queryText, queryElem)) return;
+
+ // Solve the query hashcode & return on cache hit.
+ var hash = null;
+ if (this.caching && !queryElem.length) {
+ hash = this._openArgs.username + ':' + this._openArgs.password + '@' + this._openArgs.url + '|' + queryText + "#" + this._openArgs.method;
+ var cache = JsHttpRequest.CACHE[hash];
+ if (cache) {
+ this._dataReady(cache[0], cache[1]);
+ return false;
+ }
+ }
+
+ // Try all the loaders.
+ var loader = (this.loader || '').toLowerCase();
+ if (loader && !JsHttpRequest.LOADERS[loader]) return this._error('unk_loader', loader);
+ var errors = [];
+ var lds = JsHttpRequest.LOADERS;
+ for (var tryLoader in lds) {
+ var ldr = lds[tryLoader].loader;
+ if (!ldr) continue; // exclude possibly derived prototype properties from "for .. in".
+ if (loader && tryLoader != loader) continue;
+ // Create sending context.
+ var ldObj = new ldr(this);
+ JsHttpRequest.extend(ldObj, this._openArgs);
+ JsHttpRequest.extend(ldObj, {
+ queryText: queryText.join('&'),
+ queryElem: queryElem,
+ id: (new Date().getTime()) + "" + JsHttpRequest.COUNT++,
+ hash: hash,
+ span: null
+ });
+ var error = ldObj.load();
+ if (!error) {
+ // Save loading script.
+ this._ldObj = ldObj;
+ JsHttpRequest.PENDING[ldObj.id] = this;
+ return true;
+ }
+ if (!loader) {
+ errors[errors.length] = '- ' + tryLoader.toUpperCase() + ': ' + this._l(error);
+ } else {
+ return this._error(error);
+ }
+ }
+
+ // If no loader matched, generate error message.
+ return tryLoader? this._error('no_loader_matched', errors.join('\n')) : this._error('no_loaders');
+ }
+
+ /**
+ * Returns all response headers (if supported).
+ */
+ t.getAllResponseHeaders = function() { with (this) {
+ return _ldObj && _ldObj.getAllResponseHeaders? _ldObj.getAllResponseHeaders() : [];
+ }}
+
+ /**
+ * Returns one response header (if supported).
+ */
+ t.getResponseHeader = function(label) { with (this) {
+ return _ldObj && _ldObj.getResponseHeader? _ldObj.getResponseHeader(label) : null;
+ }}
+
+ /**
+ * Adds a request header to a future query.
+ */
+ t.setRequestHeader = function(label, value) { with (this) {
+ _reqHeaders[_reqHeaders.length] = [label, value];
+ }}
+
+ //
+ // Internal functions.
+ //
+
+ /**
+ * Do all the work when a data is ready.
+ */
+ t._dataReady = function(text, js) { with (this) {
+ if (caching && _ldObj) JsHttpRequest.CACHE[_ldObj.hash] = [text, js];
+ responseText = responseXML = text;
+ responseJS = js;
+ if (js !== null) {
+ status = 200;
+ statusText = "OK";
+ } else {
+ status = 500;
+ statusText = "Internal Server Error";
+ }
+ _changeReadyState(2);
+ _changeReadyState(3);
+ _changeReadyState(4);
+ _cleanup();
+ }}
+
+ /**
+ * Analog of sprintf(), but translates the first parameter by _errors.
+ */
+ t._l = function(args) {
+ var i = 0, p = 0, msg = this._errors[args[0]];
+ // Cannot use replace() with a callback, because it is incompatible with IE5.
+ while ((p = msg.indexOf('%', p)) >= 0) {
+ var a = args[++i] + "";
+ msg = msg.substring(0, p) + a + msg.substring(p + 1, msg.length);
+ p += 1 + a.length;
+ }
+ return msg;
+ }
+
+ /**
+ * Called on error.
+ */
+ t._error = function(msg) {
+ msg = this._l(typeof(msg) == 'string'? arguments : msg)
+ msg = "JsHttpRequest: " + msg;
+ if (!window.Error) {
+ // Very old browser...
+ throw msg;
+ } else if ((new Error(1, 'test')).description == "test") {
+ // We MUST (!!!) pass 2 parameters to the Error() constructor for IE5.
+ throw new Error(1, msg);
+ } else {
+ // Mozilla does not support two-parameter call style.
+ throw new Error(msg);
+ }
+ }
+
+ /**
+ * Convert hash to QUERY_STRING.
+ * If next value is scalar or hash, push it to queryText.
+ * If next value is form element, push [name, element] to queryElem.
+ */
+ t._hash2query = function(content, prefix, queryText, queryElem) {
+ if (prefix == null) prefix = "";
+ if((''+typeof(content)).toLowerCase() == 'object') {
+ var formAdded = false;
+ if (content && content.parentNode && content.parentNode.appendChild && content.tagName && content.tagName.toUpperCase() == 'FORM') {
+ content = { form: content };
+ }
+ for (var k in content) {
+ var v = content[k];
+ if (v instanceof Function) continue;
+ var curPrefix = prefix? prefix + '[' + this.escape(k) + ']' : this.escape(k);
+ var isFormElement = v && v.parentNode && v.parentNode.appendChild && v.tagName;
+ if (isFormElement) {
+ var tn = v.tagName.toUpperCase();
+ if (tn == 'FORM') {
+ // FORM itself is passed.
+ formAdded = true;
+ } else if (tn == 'INPUT' || tn == 'TEXTAREA' || tn == 'SELECT') {
+ // This is a single form elemenent.
+ } else {
+ return this._error('inv_form_el', (v.name||''), v.tagName);
+ }
+ queryElem[queryElem.length] = { name: curPrefix, e: v };
+ } else if (v instanceof Object) {
+ this._hash2query(v, curPrefix, queryText, queryElem);
+ } else {
+ // We MUST skip NULL values, because there is no method
+ // to pass NULL's via GET or POST request in PHP.
+ if (v === null) continue;
+ // Convert JS boolean true and false to corresponding PHP values.
+ if (v === true) v = 1;
+ if (v === false) v = '';
+ queryText[queryText.length] = curPrefix + "=" + this.escape('' + v);
+ }
+ if (formAdded && queryElem.length > 1) {
+ return this._error('must_be_single_el');
+ }
+ }
+ } else {
+ queryText[queryText.length] = content;
+ }
+ return true;
+ }
+
+ /**
+ * Remove last used script element (clean memory).
+ */
+ t._cleanup = function() {
+ var ldObj = this._ldObj;
+ if (!ldObj) return;
+ // Mark this loading as aborted.
+ JsHttpRequest.PENDING[ldObj.id] = false;
+ var span = ldObj.span;
+ if (!span) return;
+ // Do NOT use iframe.contentWindow.back() - it is incompatible with Opera 9!
+ ldObj.span = null;
+ var closure = function() {
+ span.parentNode.removeChild(span);
+ }
+ // IE5 crashes on setTimeout(function() {...}, ...) construction! Use tmp variable.
+ JsHttpRequest.setTimeout(closure, 50);
+ }
+
+ /**
+ * Change current readyState and call trigger method.
+ */
+ t._changeReadyState = function(s, reset) { with (this) {
+ if (reset) {
+ status = statusText = responseJS = null;
+ responseText = '';
+ }
+ readyState = s;
+ if (onreadystatechange) onreadystatechange();
+ }}
+
+ /**
+ * JS escape() does not quote '+'.
+ */
+ t.escape = function(s) {
+ return escape(s).replace(new RegExp('\\+','g'), '%2B');
+ }
+}
+
+
+// Global library variables.
+JsHttpRequest.COUNT = 0; // unique ID; used while loading IDs generation
+JsHttpRequest.MAX_URL_LEN = 2000; // maximum URL length
+JsHttpRequest.CACHE = {}; // cached data
+JsHttpRequest.PENDING = {}; // pending loadings
+JsHttpRequest.LOADERS = {}; // list of supported data loaders (filled at the bottom of the file)
+JsHttpRequest._dummy = function() {}; // avoid memory leaks
+
+
+/**
+ * These functions are dirty hacks for IE 5.0 which does not increment a
+ * reference counter for an object passed via setTimeout(). So, if this
+ * object (closure function) is out of scope at the moment of timeout
+ * applying, IE 5.0 crashes.
+ */
+
+/**
+ * Timeout wrappers storage. Used to avoid zeroing of referece counts in IE 5.0.
+ * Please note that you MUST write "window.setTimeout", not "setTimeout", else
+ * IE 5.0 crashes again. Strange, very strange...
+ */
+JsHttpRequest.TIMEOUTS = { s: window.setTimeout, c: window.clearTimeout };
+
+/**
+ * Wrapper for IE5 buggy setTimeout.
+ * Use this function instead of a usual setTimeout().
+ */
+JsHttpRequest.setTimeout = function(func, dt) {
+ // Always save inside the window object before a call (for FF)!
+ window.JsHttpRequest_tmp = JsHttpRequest.TIMEOUTS.s;
+ if (typeof(func) == "string") {
+ id = window.JsHttpRequest_tmp(func, dt);
+ } else {
+ var id = null;
+ var mediator = function() {
+ func();
+ delete JsHttpRequest.TIMEOUTS[id]; // remove circular reference
+ }
+ id = window.JsHttpRequest_tmp(mediator, dt);
+ // Store a reference to the mediator function to the global array
+ // (reference count >= 1); use timeout ID as an array key;
+ JsHttpRequest.TIMEOUTS[id] = mediator;
+ }
+ window.JsHttpRequest_tmp = null; // no delete() in IE5 for window
+ return id;
+}
+
+/**
+ * Complimental wrapper for clearTimeout.
+ * Use this function instead of usual clearTimeout().
+ */
+JsHttpRequest.clearTimeout = function(id) {
+ window.JsHttpRequest_tmp = JsHttpRequest.TIMEOUTS.c;
+ delete JsHttpRequest.TIMEOUTS[id]; // remove circular reference
+ var r = window.JsHttpRequest_tmp(id);
+ window.JsHttpRequest_tmp = null; // no delete() in IE5 for window
+ return r;
+}
+
+
+/**
+ * Global static function.
+ * Simple interface for most popular use-cases.
+ * You may also pass URLs like "GET url" or "script.GET url".
+ */
+JsHttpRequest.query = function(url, content, onready, nocache) {
+ var req = new this();
+ req.caching = !nocache;
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ onready(req.responseJS, req.responseText);
+ }
+ }
+ req.open(null, url, true);
+ req.send(content);
+}
+
+
+/**
+ * Global static function.
+ * Called by server backend script on data load.
+ */
+JsHttpRequest.dataReady = function(d) {
+ var th = this.PENDING[d.id];
+ delete this.PENDING[d.id];
+ if (th) {
+ th._dataReady(d.text, d.js);
+ } else if (th !== false) {
+ throw "dataReady(): unknown pending id: " + d.id;
+ }
+}
+
+
+// Adds all the properties of src to dest.
+JsHttpRequest.extend = function(dest, src) {
+ for (var k in src) dest[k] = src[k];
+}
+
+/**
+ * Each loader has the following properties which must be initialized:
+ * - method
+ * - url
+ * - asyncFlag (ignored)
+ * - username
+ * - password
+ * - queryText (string)
+ * - queryElem (array)
+ * - id
+ * - hash
+ * - span
+ */
+
+// }}}
+
+// {{{ xml
+// Loader: XMLHttpRequest or ActiveX.
+// [+] GET and POST methods are supported.
+// [+] Most native and memory-cheap method.
+// [+] Backend data can be browser-cached.
+// [-] Cannot work in IE without ActiveX.
+// [-] No support for loading from different domains.
+// [-] No uploading support.
+//
+JsHttpRequest.LOADERS.xml = { loader: function(req) {
+ JsHttpRequest.extend(req._errors, {
+ xml_no: 'Cannot use XMLHttpRequest or ActiveX loader: not supported',
+ xml_no_diffdom: 'Cannot use XMLHttpRequest to load data from different domain %',
+ xml_no_headers: 'Cannot use XMLHttpRequest loader or ActiveX loader, POST method: headers setting is not supported, needed to work with encodings correctly',
+ xml_no_form_upl: 'Cannot use XMLHttpRequest loader: direct form elements using and uploading are not implemented'
+ });
+
+ this.load = function() {
+ if (this.queryElem.length) return ['xml_no_form_upl'];
+
+ // XMLHttpRequest (and MS ActiveX'es) cannot work with different domains.
+ if (this.url.match(new RegExp('^([a-z]+://[^\\/]+)(.*)', 'i'))) {
+ // We MUST also check if protocols matched: cannot send from HTTP
+ // to HTTPS and vice versa.
+ if (RegExp.$1.toLowerCase() != document.location.protocol + '//' + document.location.hostname.toLowerCase()) {
+ return ['xml_no_diffdom', RegExp.$1];
+ }
+ }
+
+ // Try to obtain a loader.
+ var xr = null;
+ if (window.XMLHttpRequest) {
+ try { xr = new XMLHttpRequest() } catch(e) {}
+ } else if (window.ActiveXObject) {
+ try { xr = new ActiveXObject("Microsoft.XMLHTTP") } catch(e) {}
+ if (!xr) try { xr = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) {}
+ }
+ if (!xr) return ['xml_no'];
+
+ // Loading method detection. We cannot POST if we cannot set "octet-stream"
+ // header, because we need to process the encoded data in the backend manually.
+ var canSetHeaders = window.ActiveXObject || xr.setRequestHeader;
+ if (!this.method) this.method = canSetHeaders && this.queryText.length? 'POST' : 'GET';
+
+ // Build & validate the full URL.
+ if (this.method == 'GET') {
+ if (this.queryText) this.url += (this.url.indexOf('?') >= 0? '&' : '?') + this.queryText;
+ this.queryText = '';
+ if (this.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];
+ } else if (this.method == 'POST' && !canSetHeaders) {
+ return ['xml_no_headers'];
+ }
+
+ // Add ID to the url if we need to disable the cache.
+ this.url += (this.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + (req.caching? '0' : this.id) + '-xml';
+
+ // Assign the result handler.
+ var id = this.id;
+ xr.onreadystatechange = function() {
+ if (xr.readyState != 4) return;
+ // Avoid memory leak by removing the closure.
+ xr.onreadystatechange = JsHttpRequest._dummy;
+ req.status = null;
+ try {
+ // In case of abort() call, xr.status is unavailable and generates exception.
+ // But xr.readyState equals to 4 in this case. Stupid behaviour. :-(
+ req.status = xr.status;
+ req.responseText = xr.responseText;
+ } catch (e) {}
+ if (!req.status) return;
+ try {
+ // Prepare generator function & catch syntax errors on this stage.
+ eval('JsHttpRequest._tmp = function(id) { var d = ' + req.responseText + '; d.id = id; JsHttpRequest.dataReady(d); }');
+ } catch (e) {
+ // Note that FF 2.0 does not throw any error from onreadystatechange handler.
+ return req._error('js_invalid', req.responseText)
+ }
+ // Call associated dataReady() outside the try-catch block
+ // to pass exceptions in onreadystatechange in usual manner.
+ JsHttpRequest._tmp(id);
+ JsHttpRequest._tmp = null;
+ };
+
+ // Open & send the request.
+ xr.open(this.method, this.url, true, this.username, this.password);
+ if (canSetHeaders) {
+ // Pass pending headers.
+ for (var i = 0; i < req._reqHeaders.length; i++) {
+ xr.setRequestHeader(req._reqHeaders[i][0], req._reqHeaders[i][1]);
+ }
+ // Set non-default Content-type. We cannot use
+ // "application/x-www-form-urlencoded" here, because
+ // in PHP variable HTTP_RAW_POST_DATA is accessible only when
+ // enctype is not default (e.g., "application/octet-stream"
+ // is a good start). We parse POST data manually in backend
+ // library code. Note that Safari sets by default "x-www-form-urlencoded"
+ // header, but FF sets "text/xml" by default.
+ xr.setRequestHeader('Content-Type', 'application/octet-stream');
+ }
+ xr.send(this.queryText);
+
+ // No SPAN is used for this loader.
+ this.span = null;
+ this.xr = xr; // save for later usage on abort()
+
+ // Success.
+ return null;
+ }
+
+ // Override req.getAllResponseHeaders method.
+ this.getAllResponseHeaders = function() {
+ return this.xr.getAllResponseHeaders();
+ }
+
+ // Override req.getResponseHeader method.
+ this.getResponseHeader = function(label) {
+ return this.xr.getResponseHeader(label);
+ }
+
+ this.abort = function() {
+ this.xr.abort();
+ this.xr = null;
+ }
+}}
+// }}}
+
+
+// {{{ script
+// Loader: SCRIPT tag.
+// [+] Most cross-browser.
+// [+] Supports loading from different domains.
+// [-] Only GET method is supported.
+// [-] No uploading support.
+// [-] Backend data cannot be browser-cached.
+//
+JsHttpRequest.LOADERS.script = { loader: function(req) {
+ JsHttpRequest.extend(req._errors, {
+ script_only_get: 'Cannot use SCRIPT loader: it supports only GET method',
+ script_no_form: 'Cannot use SCRIPT loader: direct form elements using and uploading are not implemented'
+ })
+
+ this.load = function() {
+ // Move GET parameters to the URL itself.
+ if (this.queryText) this.url += (this.url.indexOf('?') >= 0? '&' : '?') + this.queryText;
+ this.url += (this.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + this.id + '-' + 'script';
+ this.queryText = '';
+
+ if (!this.method) this.method = 'GET';
+ if (this.method !== 'GET') return ['script_only_get'];
+ if (this.queryElem.length) return ['script_no_form'];
+ if (this.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];
+
+ var th = this, d = document, s = null, b = d.body;
+ if (!window.opera) {
+ // Safari, IE, FF, Opera 7.20.
+ this.span = s = d.createElement('SCRIPT');
+ var closure = function() {
+ s.language = 'JavaScript';
+ if (s.setAttribute) s.setAttribute('src', th.url); else s.src = th.url;
+ b.insertBefore(s, b.lastChild);
+ }
+ } else {
+ // Oh shit! Damned stupid Opera 7.23 does not allow to create SCRIPT
+ // element over createElement (in HEAD or BODY section or in nested SPAN -
+ // no matter): it is created deadly, and does not response the href assignment.
+ // So - always create SPAN.
+ this.span = s = d.createElement('SPAN');
+ s.style.display = 'none';
+ b.insertBefore(s, b.lastChild);
+ s.innerHTML = 'Workaround for IE.<s'+'cript></' + 'script>';
+ var closure = function() {
+ s = s.getElementsByTagName('SCRIPT')[0]; // get with timeout!
+ s.language = 'JavaScript';
+ if (s.setAttribute) s.setAttribute('src', th.url); else s.src = th.url;
+ }
+ }
+ JsHttpRequest.setTimeout(closure, 10);
+
+ // Success.
+ return null;
+ }
+}}
+// }}}
+
+
+// {{{ form
+// Loader: FORM & IFRAME.
+// [+] Supports file uploading.
+// [+] GET and POST methods are supported.
+// [+] Supports loading from different domains.
+// [-] Uses a lot of system resources.
+// [-] Backend data cannot be browser-cached.
+// [-] Pollutes browser history on some old browsers.
+//
+JsHttpRequest.LOADERS.form = { loader: function(req) {
+ JsHttpRequest.extend(req._errors, {
+ form_el_not_belong: 'Element "%" does not belong to any form!',
+ form_el_belong_diff: 'Element "%" belongs to a different form. All elements must belong to the same form!',
+ form_el_inv_enctype: 'Attribute "enctype" of the form must be "%" (for IE), "%" given.'
+ })
+
+ this.load = function() {
+ var th = this;
+
+ if (!th.method) th.method = 'POST';
+ th.url += (th.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + th.id + '-' + 'form';
+
+ // If GET, build full URL. Then copy QUERY_STRING to queryText.
+ if (th.method == 'GET') {
+ if (th.queryText) th.url += (th.url.indexOf('?') >= 0? '&' : '?') + th.queryText;
+ if (th.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN];
+ var p = th.url.split('?', 2);
+ th.url = p[0];
+ th.queryText = p[1] || '';
+ }
+
+ // Check if all form elements belong to same form.
+ var form = null;
+ var wholeFormSending = false;
+ if (th.queryElem.length) {
+ if (th.queryElem[0].e.tagName.toUpperCase() == 'FORM') {
+ // Whole FORM sending.
+ form = th.queryElem[0].e;
+ wholeFormSending = true;
+ th.queryElem = [];
+ } else {
+ // If we have at least one form element, we use its FORM as a POST container.
+ form = th.queryElem[0].e.form;
+ // Validate all the elements.
+ for (var i = 0; i < th.queryElem.length; i++) {
+ var e = th.queryElem[i].e;
+ if (!e.form) {
+ return ['form_el_not_belong', e.name];
+ }
+ if (e.form != form) {
+ return ['form_el_belong_diff', e.name];
+ }
+ }
+ }
+
+ // Check enctype of the form.
+ if (th.method == 'POST') {
+ var need = "multipart/form-data";
+ var given = (form.attributes.encType && form.attributes.encType.nodeValue) || (form.attributes.enctype && form.attributes.enctype.value) || form.enctype;
+ if (given != need) {
+ return ['form_el_inv_enctype', need, given];
+ }
+ }
+ }
+
+ // Create invisible IFRAME with temporary form (form is used on empty queryElem).
+ // We ALWAYS create th IFRAME in the document of the form - for Opera 7.20.
+ var d = form && (form.ownerDocument || form.document) || document;
+ var ifname = 'jshr_i_' + th.id;
+ var s = th.span = d.createElement('DIV');
+ s.style.position = 'absolute';
+ s.style.display = 'none';
+ s.style.visibility = 'hidden';
+ s.innerHTML =
+ (form? '' : '<form' + (th.method == 'POST'? ' enctype="multipart/form-data" method="post"' : '') + '></form>') + // stupid IE, MUST use innerHTML assignment :-(
+ '<iframe name="' + ifname + '" id="' + ifname + '" style="width:0px; height:0px; overflow:hidden; border:none"></iframe>'
+ if (!form) {
+ form = th.span.firstChild;
+ }
+
+ // Insert generated form inside the document.
+ // Be careful: don't forget to close FORM container in document body!
+ d.body.insertBefore(s, d.body.lastChild);
+
+ // Function to safely set the form attributes. Parameter attr is NOT a hash
+ // but an array, because "for ... in" may badly iterate over derived attributes.
+ var setAttributes = function(e, attr) {
+ var sv = [];
+ var form = e;
+ // This strange algorythm is needed, because form may contain element
+ // with name like 'action'. In IE for such attribute will be returned
+ // form element node, not form action. Workaround: copy all attributes
+ // to new empty form and work with it, then copy them back. This is
+ // THE ONLY working algorythm since a lot of bugs in IE5.0 (e.g.
+ // with e.attributes property: causes IE crash).
+ if (e.mergeAttributes) {
+ var form = d.createElement('form');
+ form.mergeAttributes(e, false);
+ }
+ for (var i = 0; i < attr.length; i++) {
+ var k = attr[i][0], v = attr[i][1];
+ // TODO: http://forum.dklab.ru/viewtopic.php?p=129059#129059
+ sv[sv.length] = [k, form.getAttribute(k)];
+ form.setAttribute(k, v);
+ }
+ if (e.mergeAttributes) {
+ e.mergeAttributes(form, false);
+ }
+ return sv;
+ }
+
+ // Run submit with delay - for old Opera: it needs some time to create IFRAME.
+ var closure = function() {
+ // Save JsHttpRequest object to new IFRAME.
+ top.JsHttpRequestGlobal = JsHttpRequest;
+
+ // Disable ALL the form elements.
+ var savedNames = [];
+ if (!wholeFormSending) {
+ for (var i = 0, n = form.elements.length; i < n; i++) {
+ savedNames[i] = form.elements[i].name;
+ form.elements[i].name = '';
+ }
+ }
+
+ // Insert hidden fields to the form.
+ var qt = th.queryText.split('&');
+ for (var i = qt.length - 1; i >= 0; i--) {
+ var pair = qt[i].split('=', 2);
+ var e = d.createElement('INPUT');
+ e.type = 'hidden';
+ e.name = unescape(pair[0]);
+ e.value = pair[1] != null? unescape(pair[1]) : '';
+ form.appendChild(e);
+ }
+
+
+ // Change names of along user-passed form elements.
+ for (var i = 0; i < th.queryElem.length; i++) {
+ th.queryElem[i].e.name = th.queryElem[i].name;
+ }
+
+ // Temporary modify form attributes, submit form, restore attributes back.
+ var sv = setAttributes(
+ form,
+ [
+ ['action', th.url],
+ ['method', th.method],
+ ['onsubmit', null],
+ ['target', ifname]
+ ]
+ );
+ form.submit();
+ setAttributes(form, sv);
+
+ // Remove generated temporary hidden elements from the top of the form.
+ for (var i = 0; i < qt.length; i++) {
+ // Use "form.firstChild.parentNode", not "form", or IE5 crashes!
+ form.lastChild.parentNode.removeChild(form.lastChild);
+ }
+ // Enable all disabled elements back.
+ if (!wholeFormSending) {
+ for (var i = 0, n = form.elements.length; i < n; i++) {
+ form.elements[i].name = savedNames[i];
+ }
+ }
+ }
+ JsHttpRequest.setTimeout(closure, 100);
+
+ // Success.
+ return null;
+ }
+}}
+// }}}
+