Client side ajax library
authorJanusz Dobrowolski <janusz@frontaccounting.eu>
Fri, 16 May 2008 16:52:26 +0000 (16:52 +0000)
committerJanusz Dobrowolski <janusz@frontaccounting.eu>
Fri, 16 May 2008 16:52:26 +0000 (16:52 +0000)
js/JsHttpRequest.js [new file with mode: 0644]
js/utils.js [new file with mode: 0644]

diff --git a/js/JsHttpRequest.js b/js/JsHttpRequest.js
new file mode 100644 (file)
index 0000000..3ec48fd
--- /dev/null
@@ -0,0 +1,798 @@
+/**
+ * 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;
+    }    
+}}
+// }}}
+
diff --git a/js/utils.js b/js/utils.js
new file mode 100644 (file)
index 0000000..7c071c3
--- /dev/null
@@ -0,0 +1,165 @@
+//
+//     JsHttpRequest class extensions.
+//
+    JsHttpRequest.request= function(submit) {
+        JsHttpRequest.query(
+            'POST '+window.location.toString(), // backend
+
+           this.formValues(submit),
+                       
+            // Function is called when an answer arrives. 
+           function(result, errors) {
+                       // Write errors to the debug div.
+            document.getElementById('msgbox').innerHTML = errors; 
+                // Write the answer.
+               if (result) {
+                   for(var i in result ) { 
+                       atom = result[i];
+                       
+                       cmd = atom['n'];
+                       property = atom['p'];
+                       type = atom['c'];
+                       id = atom['t'];
+                       data = atom['data'];
+//                     alert(cmd+':'+property+':'+type+':'+id+':'+data);
+                       // seek element by id if there is no elemnt with given name
+                       objElement = document.getElementsByName(id)[0] || document.getElementById(id);
+               if(cmd=='as') {
+                                 eval("objElement."+property+"=data;");
+                       } else if(cmd=='up') {
+                         if(!objElement) debug('No element "'+id+'"');
+                           if (objElement.tagName == 'INPUT' || objElement.tagName == 'TEXTAREA')
+                                 objElement.value = data;
+                           else
+                                 objElement.innerHTML = data; // selector, div, span etc
+                       } else if(cmd=='di') { // disable element
+                                 objElement.disabled = data;
+                       } else if(cmd=='js') {  // evaluate js code
+                                 eval(data);
+                       } else if(cmd=='rd') {  // client-side redirection
+                           debug('redirecting '+data);
+                                 window.location = data;
+                       } else {
+                                 errors = errors+'<br>Unknown ajax function: '+cmd;
+                       }
+                   }
+                       Behaviour.apply();
+                       if (errors.length>0)
+                       // window.scrollTo(0,0);
+                       document.getElementById('msgbox').scrollIntoView(true);
+               }
+            },
+            false  // do not disable caching
+        );
+    }
+/*     //      calls form validation function
+       //
+    JsHttpRequest.validate= function(submit) {
+        JsHttpRequest.query(
+          'POST '+window.location.toString(), // backend
+           this.formValues('_validate_form'),
+            // Function is called when an answer arrives. 
+           function(result, errors) {
+                 if (result) {   
+                   window.location = result;
+                 } else
+                       return false;
+               return true;
+           },
+           false
+       );
+    }
+*/     // returns input field values submitted when form button 'name' is pressed
+       //
+       JsHttpRequest.formValues = function(inp)
+       {
+               var objForm;
+               var submitObj;
+               var q = {};
+               
+               if (typeof(inp) == "string")
+                       submitObj = document.getElementsByName(inp)[0];
+               else
+                       submitObj = inp;
+               if(submitObj) {
+                 objForm = submitObj.form;
+               }
+               if (objForm)
+               {
+                       var formElements = objForm.elements;
+                       for( var i=0; i < formElements.length; i++)
+                       {
+                         var el = formElements[i];
+                               if (!el.name) continue;
+                               if (el.type )
+                                 if( 
+                                 ((el.type == 'radio' || el.type == 'checkbox') && el.checked == false)
+                                 || (el.type == 'submit' && el.name!=submitObj.name))
+                                       continue;
+                               if (el.disabled && el.disabled == true)
+                                       continue;
+                               var name = el.name;
+                               if (name)
+                               {
+                                       if(el.type=='select-multiple')
+                                       {
+                                               for (var j = 0; j < el.length; j++)
+                                               {
+                                                       if (el.options[j].selected == true)
+                                                               q[name] = el.options[j].value;
+                                               }
+                                       }
+                                       else
+                                       {
+                                               q[name] = el.value;
+                                       }
+                               } 
+                       }
+               }
+               // this is to avoid caching problems
+               q['_random'] = Math.random()*1234567;
+               return q;
+       }
+//
+//     User price formatting
+//
+function price_format(post, num, dec, label) {
+       //num = num.toString().replace(/\$|\,/g,'');
+       if(isNaN(num))
+               num = "0";
+       sign = (num == (num = Math.abs(num)));
+       if(dec<0) dec = 2;
+       decsize = Math.pow(10, dec);
+       num = Math.floor(num*decsize+0.50000000001);
+       cents = num%decsize;
+       num = Math.floor(num/decsize).toString();
+       for( i=cents.toString().length; i<dec; i++){
+               cents = "0" + cents;
+       }
+       for (var i = 0; i < Math.floor((num.length-(1+i))/3); i++)
+               num = num.substring(0,num.length-(4*i+3))+user.ts+
+                       num.substring(num.length-(4*i+3));
+        num = ((sign)?'':'-') + num;
+        if(dec!=0) num = num + user.ds + cents;
+       if(label)
+           document.getElementById(post).innerHTML = num;
+       else
+           document.getElementsByName(post)[0].value = num;
+       }
+       function get_amount(doc, label) {
+           if(label)
+               var val = document.getElementById(doc).innerHTML;
+           else
+               var val = document.getElementsByName(doc)[0].value;
+               val = val.replace(new RegExp('\\'+user.ts, 'g'),'');
+               val = val.replace(new RegExp('\\'+user.ds, 'g'),'.');
+               return 1*val;
+       }
+
+function goBack() {
+       if (window.history.length <= 1)
+        window.close();
+       else
+        window.history.go(-1);
+}
+       
\ No newline at end of file