1/** 2 * JsHttpRequest: JavaScript "AJAX" data loader 3 * 4 * @license LGPL 5 * @author Dmitry Koterov, http://en.dklab.ru/lib/JsHttpRequest/ 6 * @version 5.x $Id$ 7 */ 8 9// {{{ 10function JsHttpRequest() { 11 // Standard properties. 12 var t = this; 13 t.onreadystatechange = null; 14 t.readyState = 0; 15 t.responseText = null; 16 t.responseXML = null; 17 t.status = 200; 18 t.statusText = "OK"; 19 // JavaScript response array/hash 20 t.responseJS = null; 21 22 // Additional properties. 23 t.caching = false; // need to use caching? 24 t.loader = null; // loader to use ('form', 'script', 'xml'; null - autodetect) 25 t.session_name = "PHPSESSID"; // set to SID cookie or GET parameter name 26 27 // Internals. 28 t._ldObj = null; // used loader object 29 t._reqHeaders = []; // collected request headers 30 t._openArgs = null; // parameters from open() 31 t._errors = { 32 inv_form_el: 'Invalid FORM element detected: name=%, tag=%', 33 must_be_single_el: 'If used, <form> must be a single HTML element in the list.', 34 js_invalid: 'JavaScript code generated by backend is invalid!\n%', 35 url_too_long: 'Cannot use so long query with GET request (URL is larger than % bytes)', 36 unk_loader: 'Unknown loader: %', 37 no_loaders: 'No loaders registered at all, please check JsHttpRequest.LOADERS array', 38 no_loader_matched: 'Cannot find a loader which may process the request. Notices are:\n%', 39 no_headers: 'Method setRequestHeader() cannot work together with the % loader.' 40 } 41 42 /** 43 * Aborts the request. Behaviour of this function for onreadystatechange() 44 * is identical to IE (most universal and common case). E.g., readyState -> 4 45 * on abort() after send(). 46 */ 47 t.abort = function() { with (this) { 48 if (_ldObj && _ldObj.abort) _ldObj.abort(); 49 _cleanup(); 50 if (readyState == 0) { 51 // start->abort: no change of readyState (IE behaviour) 52 return; 53 } 54 if (readyState == 1 && !_ldObj) { 55 // open->abort: no onreadystatechange call, but change readyState to 0 (IE). 56 // send->abort: change state to 4 (_ldObj is not null when send() is called) 57 readyState = 0; 58 return; 59 } 60 _changeReadyState(4, true); // 4 in IE & FF on abort() call; Opera does not change to 4. 61 }} 62 63 /** 64 * Prepares the object for data loading. 65 */ 66 t.open = function(method, url, asyncFlag, username, password) { with (this){ 67 // Append SID to original URL. Use try...catch for security problems. 68 try { 69 if ( 70 document.location.search.match(new RegExp('[&?]' + session_name + '=([^&?]*)')) 71 || document.cookie.match(new RegExp('(?:;|^)\\s*' + session_name + '=([^;]*)')) 72 ) { 73 url += (url.indexOf('?') >= 0? '&' : '?') + session_name + "=" + this.escape(RegExp.$1); 74 } 75 } catch (e) {} 76 // Store open arguments to hash. 77 _openArgs = { 78 method: (method || '').toUpperCase(), 79 url: url, 80 asyncFlag: asyncFlag, 81 username: username != null? username : '', 82 password: password != null? password : '' 83 } 84 _ldObj = null; 85 _changeReadyState(1, true); // compatibility with XMLHttpRequest 86 return true; 87 }} 88 89 /** 90 * Sends a request to a server. 91 */ 92 t.send = function(content) { 93 if (!this.readyState) { 94 // send without open or after abort: no action (IE behaviour). 95 return; 96 } 97 this._changeReadyState(1, true); // compatibility with XMLHttpRequest 98 this._ldObj = null; 99 100 // Prepare to build QUERY_STRING from query hash. 101 var queryText = []; 102 var queryElem = []; 103 if (!this._hash2query(content, null, queryText, queryElem)) return; 104 105 // Solve the query hashcode & return on cache hit. 106 var hash = null; 107 if (this.caching && !queryElem.length) { 108 hash = this._openArgs.username + ':' + this._openArgs.password + '@' + this._openArgs.url + '|' + queryText + "#" + this._openArgs.method; 109 var cache = JsHttpRequest.CACHE[hash]; 110 if (cache) { 111 this._dataReady(cache[0], cache[1]); 112 return false; 113 } 114 } 115 116 // Try all the loaders. 117 var loader = (this.loader || '').toLowerCase(); 118 if (loader && !JsHttpRequest.LOADERS[loader]) return this._error('unk_loader', loader); 119 var errors = []; 120 var lds = JsHttpRequest.LOADERS; 121 for (var tryLoader in lds) { 122 var ldr = lds[tryLoader].loader; 123 if (!ldr) continue; // exclude possibly derived prototype properties from "for .. in". 124 if (loader && tryLoader != loader) continue; 125 // Create sending context. 126 var ldObj = new ldr(this); 127 JsHttpRequest.extend(ldObj, this._openArgs); 128 JsHttpRequest.extend(ldObj, { 129 queryText: queryText.join('&'), 130 queryElem: queryElem, 131 id: (new Date().getTime()) + "" + JsHttpRequest.COUNT++, 132 hash: hash, 133 span: null 134 }); 135 var error = ldObj.load(); 136 if (!error) { 137 // Save loading script. 138 this._ldObj = ldObj; 139 JsHttpRequest.PENDING[ldObj.id] = this; 140 return true; 141 } 142 if (!loader) { 143 errors[errors.length] = '- ' + tryLoader.toUpperCase() + ': ' + this._l(error); 144 } else { 145 return this._error(error); 146 } 147 } 148 149 // If no loader matched, generate error message. 150 return tryLoader? this._error('no_loader_matched', errors.join('\n')) : this._error('no_loaders'); 151 } 152 153 /** 154 * Returns all response headers (if supported). 155 */ 156 t.getAllResponseHeaders = function() { with (this) { 157 return _ldObj && _ldObj.getAllResponseHeaders? _ldObj.getAllResponseHeaders() : []; 158 }} 159 160 /** 161 * Returns one response header (if supported). 162 */ 163 t.getResponseHeader = function(label) { with (this) { 164 return _ldObj && _ldObj.getResponseHeader? _ldObj.getResponseHeader() : []; 165 }} 166 167 /** 168 * Adds a request header to a future query. 169 */ 170 t.setRequestHeader = function(label, value) { with (this) { 171 _reqHeaders[_reqHeaders.length] = [label, value]; 172 }} 173 174 // 175 // Internal functions. 176 // 177 178 /** 179 * Do all the work when a data is ready. 180 */ 181 t._dataReady = function(text, js) { with (this) { 182 if (caching && _ldObj) JsHttpRequest.CACHE[_ldObj.hash] = [text, js]; 183 if (text !== null || js !== null) { 184 status = 4; 185 responseText = responseXML = text; 186 responseJS = js; 187 } else { 188 status = 500; 189 responseText = responseXML = responseJS = null; 190 } 191 _changeReadyState(2); 192 _changeReadyState(3); 193 _changeReadyState(4); 194 _cleanup(); 195 }} 196 197 /** 198 * Analog of sprintf(), but translates the first parameter by _errors. 199 */ 200 t._l = function(args) { 201 var i = 0, p = 0, msg = this._errors[args[0]]; 202 // Cannot use replace() with a callback, because it is incompatible with IE5. 203 while ((p = msg.indexOf('%', p)) >= 0) { 204 var a = args[++i] + ""; 205 msg = msg.substring(0, p) + a + msg.substring(p + 1, msg.length); 206 p += 1 + a.length; 207 } 208 return msg; 209 } 210 211 /** 212 * Called on error. 213 */ 214 t._error = function(msg) { 215 msg = this._l(typeof(msg) == 'string'? arguments : msg) 216 msg = "JsHttpRequest: " + msg; 217 if (!window.Error) { 218 // Very old browser... 219 throw msg; 220 } else if ((new Error(1, 'test')).description == "test") { 221 // We MUST (!!!) pass 2 parameters to the Error() constructor for IE5. 222 throw new Error(1, msg); 223 } else { 224 // Mozilla does not support two-parameter call style. 225 throw new Error(msg); 226 } 227 } 228 229 /** 230 * Convert hash to QUERY_STRING. 231 * If next value is scalar or hash, push it to queryText. 232 * If next value is form element, push [name, element] to queryElem. 233 */ 234 t._hash2query = function(content, prefix, queryText, queryElem) { 235 if (prefix == null) prefix = ""; 236 if((''+typeof(content)).toLowerCase() == 'object') { 237 var formAdded = false; 238 if (content && content.parentNode && content.parentNode.appendChild && content.tagName && content.tagName.toUpperCase() == 'FORM') { 239 content = { form: content }; 240 } 241 for (var k in content) { 242 var v = content[k]; 243 if (v instanceof Function) continue; 244 var curPrefix = prefix? prefix + '[' + this.escape(k) + ']' : this.escape(k); 245 var isFormElement = v && v.parentNode && v.parentNode.appendChild && v.tagName; 246 if (isFormElement) { 247 var tn = v.tagName.toUpperCase(); 248 if (tn == 'FORM') { 249 // FORM itself is passed. 250 formAdded = true; 251 } else if (tn == 'INPUT' || tn == 'TEXTAREA' || tn == 'SELECT') { 252 // This is a single form elemenent. 253 } else { 254 return this._error('inv_form_el', (v.name||''), v.tagName); 255 } 256 queryElem[queryElem.length] = { name: curPrefix, e: v }; 257 } else if (v instanceof Object) { 258 this._hash2query(v, curPrefix, queryText, queryElem); 259 } else { 260 // We MUST skip NULL values, because there is no method 261 // to pass NULL's via GET or POST request in PHP. 262 if (v === null) continue; 263 queryText[queryText.length] = curPrefix + "=" + this.escape('' + v); 264 } 265 if (formAdded && queryElem.length > 1) { 266 return this._error('must_be_single_el'); 267 } 268 } 269 } else { 270 queryText[queryText.length] = content; 271 } 272 return true; 273 } 274 275 /** 276 * Remove last used script element (clean memory). 277 */ 278 t._cleanup = function() { 279 var ldObj = this._ldObj; 280 if (!ldObj) return; 281 // Mark this loading as aborted. 282 JsHttpRequest.PENDING[ldObj.id] = false; 283 var span = ldObj.span; 284 if (!span) return; 285 ldObj.span = null; 286 var closure = function() { 287 span.parentNode.removeChild(span); 288 } 289 // IE5 crashes on setTimeout(function() {...}, ...) construction! Use tmp variable. 290 JsHttpRequest.setTimeout(closure, 50); 291 } 292 293 /** 294 * Change current readyState and call trigger method. 295 */ 296 t._changeReadyState = function(s, reset) { with (this) { 297 if (reset) { 298 status = statusText = responseJS = null; 299 responseText = ''; 300 } 301 readyState = s; 302 if (onreadystatechange) onreadystatechange(); 303 }} 304 305 /** 306 * JS escape() does not quote '+'. 307 */ 308 t.escape = function(s) { 309 return escape(s).replace(new RegExp('\\+','g'), '%2B'); 310 } 311} 312 313 314// Global library variables. 315JsHttpRequest.COUNT = 0; // unique ID; used while loading IDs generation 316JsHttpRequest.MAX_URL_LEN = 2000; // maximum URL length 317JsHttpRequest.CACHE = {}; // cached data 318JsHttpRequest.PENDING = {}; // pending loadings 319JsHttpRequest.LOADERS = {}; // list of supported data loaders (filled at the bottom of the file) 320JsHttpRequest._dummy = function() {}; // avoid memory leaks 321 322 323/** 324 * These functions are dirty hacks for IE 5.0 which does not increment a 325 * reference counter for an object passed via setTimeout(). So, if this 326 * object (closure function) is out of scope at the moment of timeout 327 * applying, IE 5.0 crashes. 328 */ 329 330/** 331 * Timeout wrappers storage. Used to avoid zeroing of referece counts in IE 5.0. 332 * Please note that you MUST write "window.setTimeout", not "setTimeout", else 333 * IE 5.0 crashes again. Strange, very strange... 334 */ 335JsHttpRequest.TIMEOUTS = { s: window.setTimeout, c: window.clearTimeout }; 336 337/** 338 * Wrapper for IE5 buggy setTimeout. 339 * Use this function instead of a usual setTimeout(). 340 */ 341JsHttpRequest.setTimeout = function(func, dt) { 342 // Always save inside the window object before a call (for FF)! 343 window.JsHttpRequest_tmp = JsHttpRequest.TIMEOUTS.s; 344 if (typeof(func) == "string") { 345 id = window.JsHttpRequest_tmp(func, dt); 346 } else { 347 var id = null; 348 var mediator = function() { 349 func(); 350 delete JsHttpRequest.TIMEOUTS[id]; // remove circular reference 351 } 352 id = window.JsHttpRequest_tmp(mediator, dt); 353 // Store a reference to the mediator function to the global array 354 // (reference count >= 1); use timeout ID as an array key; 355 JsHttpRequest.TIMEOUTS[id] = mediator; 356 } 357 window.JsHttpRequest_tmp = null; // no delete() in IE5 for window 358 return id; 359} 360 361/** 362 * Complimental wrapper for clearTimeout. 363 * Use this function instead of usual clearTimeout(). 364 */ 365JsHttpRequest.clearTimeout = function(id) { 366 window.JsHttpRequest_tmp = JsHttpRequest.TIMEOUTS.c; 367 delete JsHttpRequest.TIMEOUTS[id]; // remove circular reference 368 var r = window.JsHttpRequest_tmp(id); 369 window.JsHttpRequest_tmp = null; // no delete() in IE5 for window 370 return r; 371} 372 373 374/** 375 * Global static function. 376 * Simple interface for most popular use-cases. 377 * You may also pass URLs like "GET url" or "script.GET url". 378 */ 379JsHttpRequest.query = function(url, content, onready, nocache) { 380 var req = new this(); 381 req.caching = !nocache; 382 req.onreadystatechange = function() { 383 if (req.readyState == 4) { 384 onready(req.responseJS, req.responseText); 385 } 386 } 387 var method = null; 388 if (url.match(/^((\w+)\.)?(GET|POST)\s+(.*)/i)) { 389 req.loader = RegExp.$2? RegExp.$2 : null; 390 method = RegExp.$3; 391 url = RegExp.$4; 392 } 393 req.open(method, url, true); 394 req.send(content); 395} 396 397 398/** 399 * Global static function. 400 * Called by server backend script on data load. 401 */ 402JsHttpRequest.dataReady = function(d) { 403 var th = this.PENDING[d.id]; 404 delete this.PENDING[d.id]; 405 if (th) { 406 th._dataReady(d.text, d.js); 407 } else if (th !== false) { 408 throw "dataReady(): unknown pending id: " + d.id; 409 } 410} 411 412 413// Adds all the properties of src to dest. 414JsHttpRequest.extend = function(dest, src) { 415 for (var k in src) dest[k] = src[k]; 416} 417 418/** 419 * Each loader has the following properties which must be initialized: 420 * - method 421 * - url 422 * - asyncFlag (ignored) 423 * - username 424 * - password 425 * - queryText (string) 426 * - queryElem (array) 427 * - id 428 * - hash 429 * - span 430 */ 431 432// }}} 433 434// {{{ xml 435// Loader: XMLHttpRequest or ActiveX. 436// [+] GET and POST methods are supported. 437// [+] Most native and memory-cheap method. 438// [+] Backend data can be browser-cached. 439// [-] Cannot work in IE without ActiveX. 440// [-] No support for loading from different domains. 441// [-] No uploading support. 442// 443JsHttpRequest.LOADERS.xml = { loader: function(req) { 444 JsHttpRequest.extend(req._errors, { 445 xml_no: 'Cannot use XMLHttpRequest or ActiveX loader: not supported', 446 xml_no_diffdom: 'Cannot use XMLHttpRequest to load data from different domain %', 447 xml_no_headers: 'Cannot use XMLHttpRequest loader or ActiveX loader, POST method: headers setting is not supported, needed to work with encodings correctly', 448 xml_no_form_upl: 'Cannot use XMLHttpRequest loader: direct form elements using and uploading are not implemented' 449 }); 450 451 this.load = function() { 452 if (this.queryElem.length) return ['xml_no_form_upl']; 453 454 // XMLHttpRequest (and MS ActiveX'es) cannot work with different domains. 455 if (this.url.match(new RegExp('^([a-z]+)://([^\\/]+)(.*)', 'i'))) { 456 if (RegExp.$2.toLowerCase() == document.location.hostname.toLowerCase()) { 457 this.url = RegExp.$3; 458 } else { 459 return ['xml_no_diffdom', RegExp.$2]; 460 } 461 } 462 463 // Try to obtain a loader. 464 var xr = null; 465 if (window.XMLHttpRequest) { 466 try { xr = new XMLHttpRequest() } catch(e) {} 467 } else if (window.ActiveXObject) { 468 try { xr = new ActiveXObject("Microsoft.XMLHTTP") } catch(e) {} 469 if (!xr) try { xr = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) {} 470 } 471 if (!xr) return ['xml_no']; 472 473 // Loading method detection. We cannot POST if we cannot set "octet-stream" 474 // header, because we need to process the encoded data in the backend manually. 475 var canSetHeaders = window.ActiveXObject || xr.setRequestHeader; 476 if (!this.method) this.method = canSetHeaders? 'POST' : 'GET'; 477 478 // Build & validate the full URL. 479 if (this.method == 'GET') { 480 if (this.queryText) this.url += (this.url.indexOf('?') >= 0? '&' : '?') + this.queryText; 481 this.queryText = ''; 482 if (this.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN]; 483 } else if (this.method == 'POST' && !canSetHeaders) { 484 return ['xml_no_headers']; 485 } 486 487 // Add ID to the url if we need to disable the cache. 488 this.url += (this.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + (req.caching? '0' : this.id) + '-xml'; 489 490 // Assign the result handler. 491 var id = this.id; 492 xr.onreadystatechange = function() { 493 if (xr.readyState != 4) return; 494 // Avoid memory leak by removing the closure. 495 xr.onreadystatechange = JsHttpRequest._dummy; 496 req.status = null; 497 try { 498 // In case of abort() call, xr.status is unavailable and generates exception. 499 // But xr.readyState equals to 4 in this case. Stupid behaviour. :-( 500 req.status = xr.status; 501 req.responseText = xr.responseText; 502 } catch (e) {} 503 if (!req.status) return; 504 try { 505 // Prepare generator function & catch syntax errors on this stage. 506 eval('JsHttpRequest._tmp = function(id) { var d = ' + req.responseText + '; d.id = id; JsHttpRequest.dataReady(d); }'); 507 } catch (e) { 508 // Note that FF 2.0 does not throw any error from onreadystatechange handler. 509 return req._error('js_invalid', req.responseText) 510 } 511 // Call associated dataReady() outside the try-catch block 512 // to pass exceptions in onreadystatechange in usual manner. 513 JsHttpRequest._tmp(id); 514 JsHttpRequest._tmp = null; 515 }; 516 517 // Open & send the request. 518 xr.open(this.method, this.url, true, this.username, this.password); 519 if (canSetHeaders) { 520 // Pass pending headers. 521 for (var i = 0; i < req._reqHeaders.length; i++) { 522 xr.setRequestHeader(req._reqHeaders[i][0], req._reqHeaders[i][1]); 523 } 524 // Set non-default Content-type. We cannot use 525 // "application/x-www-form-urlencoded" here, because 526 // in PHP variable HTTP_RAW_POST_DATA is accessible only when 527 // enctype is not default (e.g., "application/octet-stream" 528 // is a good start). We parse POST data manually in backend 529 // library code. Note that Safari sets by default "x-www-form-urlencoded" 530 // header, but FF sets "text/xml" by default. 531 xr.setRequestHeader('Content-Type', 'application/octet-stream'); 532 } 533 xr.send(this.queryText); 534 535 // No SPAN is used for this loader. 536 this.span = null; 537 this.xr = xr; // save for later usage on abort() 538 539 // Success. 540 return null; 541 } 542 543 // Override req.getAllResponseHeaders method. 544 this.getAllResponseHeaders = function() { 545 return this.xr.getAllResponseHeaders(); 546 } 547 548 // Override req.getResponseHeader method. 549 this.getResponseHeader = function(label) { 550 return this.xr.getResponseHeader(label); 551 } 552 553 this.abort = function() { 554 this.xr.abort(); 555 this.xr = null; 556 } 557}} 558// }}} 559 560 561// {{{ script 562// Loader: SCRIPT tag. 563// [+] Most cross-browser. 564// [+] Supports loading from different domains. 565// [-] Only GET method is supported. 566// [-] No uploading support. 567// [-] Backend data cannot be browser-cached. 568// 569JsHttpRequest.LOADERS.script = { loader: function(req) { 570 JsHttpRequest.extend(req._errors, { 571 script_only_get: 'Cannot use SCRIPT loader: it supports only GET method', 572 script_no_form: 'Cannot use SCRIPT loader: direct form elements using and uploading are not implemented' 573 }) 574 575 this.load = function() { 576 // Move GET parameters to the URL itself. 577 if (this.queryText) this.url += (this.url.indexOf('?') >= 0? '&' : '?') + this.queryText; 578 this.url += (this.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + this.id + '-' + 'script'; 579 this.queryText = ''; 580 581 if (!this.method) this.method = 'GET'; 582 if (this.method !== 'GET') return ['script_only_get']; 583 if (this.queryElem.length) return ['script_no_form']; 584 if (this.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN]; 585 if (req._reqHeaders.length) return ['no_headers', 'SCRIPT']; 586 587 var th = this, d = document, s = null, b = d.body; 588 if (!window.opera) { 589 // Safari, IE, FF, Opera 7.20. 590 this.span = s = d.createElement('SCRIPT'); 591 var closure = function() { 592 s.language = 'JavaScript'; 593 if (s.setAttribute) s.setAttribute('src', th.url); else s.src = th.url; 594 b.insertBefore(s, b.lastChild); 595 } 596 } else { 597 // Oh shit! Damned stupid Opera 7.23 does not allow to create SCRIPT 598 // element over createElement (in HEAD or BODY section or in nested SPAN - 599 // no matter): it is created deadly, and does not response the href assignment. 600 // So - always create SPAN. 601 this.span = s = d.createElement('SPAN'); 602 s.style.display = 'none'; 603 b.insertBefore(s, b.lastChild); 604 s.innerHTML = 'Workaround for IE.<s'+'cript></' + 'script>'; 605 var closure = function() { 606 s = s.getElementsByTagName('SCRIPT')[0]; // get with timeout! 607 s.language = 'JavaScript'; 608 if (s.setAttribute) s.setAttribute('src', th.url); else s.src = th.url; 609 } 610 } 611 JsHttpRequest.setTimeout(closure, 10); 612 613 // Success. 614 return null; 615 } 616}} 617// }}} 618 619 620// {{{ form 621// Loader: FORM & IFRAME. 622// [+] Supports file uploading. 623// [+] GET and POST methods are supported. 624// [+] Supports loading from different domains. 625// [-] Uses a lot of system resources. 626// [-] Backend data cannot be browser-cached. 627// [-] Pollutes browser history on some old browsers. 628// 629JsHttpRequest.LOADERS.form = { loader: function(req) { 630 JsHttpRequest.extend(req._errors, { 631 form_el_not_belong: 'Element "%" does not belong to any form!', 632 form_el_belong_diff: 'Element "%" belongs to a different form. All elements must belong to the same form!', 633 form_el_inv_enctype: 'Attribute "enctype" of the form must be "%" (for IE), "%" given.' 634 }) 635 636 this.load = function() { 637 var th = this; 638 639 if (!th.method) th.method = 'POST'; 640 th.url += (th.url.indexOf('?') >= 0? '&' : '?') + 'JsHttpRequest=' + th.id + '-' + 'form'; 641 642 if (req._reqHeaders.length) return ['no_headers', 'FORM']; 643 644 // If GET, build full URL. Then copy QUERY_STRING to queryText. 645 if (th.method == 'GET') { 646 if (th.queryText) th.url += (th.url.indexOf('?') >= 0? '&' : '?') + th.queryText; 647 if (th.url.length > JsHttpRequest.MAX_URL_LEN) return ['url_too_long', JsHttpRequest.MAX_URL_LEN]; 648 var p = th.url.split('?', 2); 649 th.url = p[0]; 650 th.queryText = p[1] || ''; 651 } 652 653 // Check if all form elements belong to same form. 654 var form = null; 655 var wholeFormSending = false; 656 if (th.queryElem.length) { 657 if (th.queryElem[0].e.tagName.toUpperCase() == 'FORM') { 658 // Whole FORM sending. 659 form = th.queryElem[0].e; 660 wholeFormSending = true; 661 th.queryElem = []; 662 } else { 663 // If we have at least one form element, we use its FORM as a POST container. 664 form = th.queryElem[0].e.form; 665 // Validate all the elements. 666 for (var i = 0; i < th.queryElem.length; i++) { 667 var e = th.queryElem[i].e; 668 if (!e.form) { 669 return ['form_el_not_belong', e.name]; 670 } 671 if (e.form != form) { 672 return ['form_el_belong_diff', e.name]; 673 } 674 } 675 } 676 677 // Check enctype of the form. 678 if (th.method == 'POST') { 679 var need = "multipart/form-data"; 680 var given = (form.attributes.encType && form.attributes.encType.nodeValue) || (form.attributes.enctype && form.attributes.enctype.value) || form.enctype; 681 if (given != need) { 682 return ['form_el_inv_enctype', need, given]; 683 } 684 } 685 } 686 687 // Create invisible IFRAME with temporary form (form is used on empty queryElem). 688 // We ALWAYS create th IFRAME in the document of the form - for Opera 7.20. 689 var d = form && (form.ownerDocument || form.document) || document; 690 var ifname = 'jshr_i_' + th.id; 691 var s = th.span = d.createElement('DIV'); 692 s.style.position = 'absolute'; 693 s.style.display = 'none'; 694 s.style.visibility = 'hidden'; 695 s.innerHTML = 696 (form? '' : '<form' + (th.method == 'POST'? ' enctype="multipart/form-data" method="post"' : '') + '></form>') + // stupid IE, MUST use innerHTML assignment :-( 697 '<iframe name="' + ifname + '" id="' + ifname + '" style="width:0px; height:0px; overflow:hidden; border:none"></iframe>' 698 if (!form) { 699 form = th.span.firstChild; 700 } 701 702 // Insert generated form inside the document. 703 // Be careful: don't forget to close FORM container in document body! 704 d.body.insertBefore(s, d.body.lastChild); 705 706 // Function to safely set the form attributes. Parameter attr is NOT a hash 707 // but an array, because "for ... in" may badly iterate over derived attributes. 708 var setAttributes = function(e, attr) { 709 var sv = []; 710 var form = e; 711 // This strange algorythm is needed, because form may contain element 712 // with name like 'action'. In IE for such attribute will be returned 713 // form element node, not form action. Workaround: copy all attributes 714 // to new empty form and work with it, then copy them back. This is 715 // THE ONLY working algorythm since a lot of bugs in IE5.0 (e.g. 716 // with e.attributes property: causes IE crash). 717 if (e.mergeAttributes) { 718 var form = d.createElement('form'); 719 form.mergeAttributes(e, false); 720 } 721 for (var i = 0; i < attr.length; i++) { 722 var k = attr[i][0], v = attr[i][1]; 723 // TODO: http://forum.dklab.ru/viewtopic.php?p=129059#129059 724 sv[sv.length] = [k, form.getAttribute(k)]; 725 form.setAttribute(k, v); 726 } 727 if (e.mergeAttributes) { 728 e.mergeAttributes(form, false); 729 } 730 return sv; 731 } 732 733 // Run submit with delay - for old Opera: it needs some time to create IFRAME. 734 var closure = function() { 735 // Save JsHttpRequest object to new IFRAME. 736 top.JsHttpRequestGlobal = JsHttpRequest; 737 738 // Disable ALL the form elements. 739 var savedNames = []; 740 if (!wholeFormSending) { 741 for (var i = 0, n = form.elements.length; i < n; i++) { 742 savedNames[i] = form.elements[i].name; 743 form.elements[i].name = ''; 744 } 745 } 746 747 // Insert hidden fields to the form. 748 var qt = th.queryText.split('&'); 749 for (var i = qt.length - 1; i >= 0; i--) { 750 var pair = qt[i].split('=', 2); 751 var e = d.createElement('INPUT'); 752 e.type = 'hidden'; 753 e.name = unescape(pair[0]); 754 e.value = pair[1] != null? unescape(pair[1]) : ''; 755 form.appendChild(e); 756 } 757 758 759 // Change names of along user-passed form elements. 760 for (var i = 0; i < th.queryElem.length; i++) { 761 th.queryElem[i].e.name = th.queryElem[i].name; 762 } 763 764 // Temporary modify form attributes, submit form, restore attributes back. 765 var sv = setAttributes( 766 form, 767 [ 768 ['action', th.url], 769 ['method', th.method], 770 ['onsubmit', null], 771 ['target', ifname] 772 ] 773 ); 774 form.submit(); 775 setAttributes(form, sv); 776 777 // Remove generated temporary hidden elements from the top of the form. 778 for (var i = 0; i < qt.length; i++) { 779 // Use "form.firstChild.parentNode", not "form", or IE5 crashes! 780 form.lastChild.parentNode.removeChild(form.lastChild); 781 } 782 // Enable all disabled elements back. 783 if (!wholeFormSending) { 784 for (var i = 0, n = form.elements.length; i < n; i++) { 785 form.elements[i].name = savedNames[i]; 786 } 787 } 788 } 789 JsHttpRequest.setTimeout(closure, 100); 790 791 // Success. 792 return null; 793 } 794}} 795// }}} 796