From f7d5b6bb6541df3591610e08bf83ccb8b107ea31 Mon Sep 17 00:00:00 2001 From: Janusz Dobrowolski Date: Fri, 16 May 2008 16:52:26 +0000 Subject: [PATCH] Client side ajax library --- js/JsHttpRequest.js | 798 ++++++++++++++++++++++++++++++++++++++++++++ js/utils.js | 165 +++++++++ 2 files changed, 963 insertions(+) create mode 100644 js/JsHttpRequest.js create mode 100644 js/utils.js diff --git a/js/JsHttpRequest.js b/js/JsHttpRequest.js new file mode 100644 index 00000000..3ec48fd3 --- /dev/null +++ b/js/JsHttpRequest.js @@ -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,
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.'; + 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? '' : '') + // stupid IE, MUST use innerHTML assignment :-( + '' + 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 index 00000000..7c071c33 --- /dev/null +++ b/js/utils.js @@ -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+'
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