Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 5448

Show
Ignore:
Timestamp:
11/07/06 06:23:31 (2 years ago)
Author:
sam
Message:

prototype: A slew of Ajax improvements. Closes #6366.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • spinoffs/prototype/CHANGELOG

    r5447 r5448  
    11*SVN* 
     2 
     3* A slew of Ajax improvements.  Closes #6366.  [mislav, sam] 
     4   
     5  Public-facing changes include: 
     6  - HTTP method can be specified in either lowercase or uppercase, and uppercase is always used when opening the XHR connection 
     7  - Added 'encoding' option (for POST) with a default of 'UTF-8' 
     8  - Ajax.Request now recognizes all the JavaScript MIME types we're aware of 
     9  - PUT body support with the 'postBody' option 
     10  - HTTP authentication support with the 'username' and 'password' options 
     11  - Query parameters can be passed as a string or as a hash 
     12  - Fixed both String.toQueryParams and Hash.toQueryString when handling empty values 
     13  - Request headers can now be specified as a hash with the 'requestHeaders' option 
    214 
    315* Improve performance of the common case where $ is called with a single argument. Closes #6347. [sam, rvermillion, mislav] 
  • spinoffs/prototype/src/ajax.js

    r4875 r5448  
    1818  }, 
    1919 
    20   register: function(responderToAdd) { 
    21     if (!this.include(responderToAdd)) 
    22       this.responders.push(responderToAdd); 
    23   }, 
    24    
    25   unregister: function(responderToRemove) { 
    26     this.responders = this.responders.without(responderToRemove); 
     20  register: function(responder) { 
     21    if (!this.include(responder)) 
     22      this.responders.push(responder); 
     23  }, 
     24   
     25  unregister: function(responder) { 
     26    this.responders = this.responders.without(responder); 
    2727  }, 
    2828   
    2929  dispatch: function(callback, request, transport, json) { 
    3030    this.each(function(responder) { 
    31       if (responder[callback] && typeof responder[callback] == 'function') { 
     31      if (typeof responder[callback] == 'function') { 
    3232        try { 
    3333          responder[callback].apply(responder, [request, transport, json]); 
     
    4343  onCreate: function() { 
    4444    Ajax.activeRequestCount++; 
    45   }, 
    46    
     45  },  
    4746  onComplete: function() { 
    4847    Ajax.activeRequestCount--; 
     
    5756      asynchronous: true, 
    5857      contentType:  'application/x-www-form-urlencoded', 
     58      encoding:     'UTF-8', 
    5959      parameters:   '' 
    6060    } 
    6161    Object.extend(this.options, options || {}); 
    62   }, 
    63  
    64   responseIsSuccess: function() { 
    65     return this.transport.status == undefined 
    66         || this.transport.status == 0  
    67         || (this.transport.status >= 200 && this.transport.status < 300); 
    68   }, 
    69  
    70   responseIsFailure: function() { 
    71     return !this.responseIsSuccess(); 
    72   } 
     62          
     63    this.options.method = this.options.method.toLowerCase(); 
     64    this.options.parameters = $H(typeof this.options.parameters == 'string' ? 
     65      this.options.parameters.toQueryParams() : this.options.parameters); 
     66  }  
    7367} 
    7468 
     
    8579 
    8680  request: function(url) { 
    87     var parameters = this.options.parameters || ''
    88     if (parameters.length > 0) parameters += '&_='; 
    89  
    90     /* Simulate other verbs over post */ 
    91     if (this.options.method != 'get' && this.options.method != 'post') { 
    92       parameters += (parameters.length > 0 ? '&' : '') + '_method=' + this.options.method; 
     81    var params = this.options.parameters
     82    if (params.any()) params['_'] = ''; 
     83 
     84    if (!['get', 'post'].include(this.options.method)) { 
     85      // simulate other verbs over post 
     86      params['_method'] = this.options.method; 
    9387      this.options.method = 'post'; 
    9488    } 
    9589     
    96     try { 
    97       this.url = url; 
    98       if (this.options.method == 'get' && parameters.length > 0) 
    99         this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; 
    100        
     90    this.url = url; 
     91     
     92    // when GET, append parameters to URL 
     93    if (this.options.method == 'get' && params.any()) 
     94      this.url += (this.url.indexOf('?') >= 0 ? '&' : '?') +  
     95        params.toQueryString(); 
     96       
     97    try { 
    10198      Ajax.Responders.dispatch('onCreate', this, this.transport); 
    102        
    103       this.transport.open(this.options.method, this.url,  
    104         this.options.asynchronous); 
     99     
     100      this.transport.open(this.options.method.toUpperCase(), this.url,  
     101        this.options.asynchronous, this.options.username,  
     102        this.options.password); 
    105103 
    106104      if (this.options.asynchronous) 
    107105        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10); 
    108  
     106       
    109107      this.transport.onreadystatechange = this.onStateChange.bind(this); 
    110108      this.setRequestHeaders(); 
    111109 
    112       var body = this.options.postBody ? this.options.postBody : parameters; 
    113       this.transport.send(this.options.method == 'post' ? body : null); 
     110      var body = this.options.method == 'post' ? 
     111        (this.options.postBody || params.toQueryString()) : null; 
     112       
     113      this.transport.send(body); 
    114114 
    115115      /* Force Firefox to handle ready state 4 for synchronous requests */ 
    116116      if (!this.options.asynchronous && this.transport.overrideMimeType) 
    117117        this.onStateChange(); 
    118          
    119     } catch (e) { 
     118    } 
     119    catch (e) { 
    120120      this.dispatchException(e); 
    121121    } 
    122   }, 
    123  
    124   setRequestHeaders: function() { 
    125     var requestHeaders =  
    126       ['X-Requested-With', 'XMLHttpRequest', 
    127        'X-Prototype-Version', Prototype.Version, 
    128        'Accept', 'text/javascript, text/html, application/xml, text/xml, */*']; 
    129  
    130     if (this.options.method == 'post') { 
    131       requestHeaders.push('Content-type', this.options.contentType); 
    132  
    133       /* Force "Connection: close" for Mozilla browsers to work around 
    134        * a bug where XMLHttpReqeuest sends an incorrect Content-length 
    135        * header. See Mozilla Bugzilla #246651.  
    136        */ 
    137       if (this.transport.overrideMimeType) 
    138         requestHeaders.push('Connection', 'close'); 
    139     } 
    140  
    141     if (this.options.requestHeaders) 
    142       requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); 
    143  
    144     for (var i = 0; i < requestHeaders.length; i += 2) 
    145       this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); 
    146122  }, 
    147123 
    148124  onStateChange: function() { 
    149125    var readyState = this.transport.readyState; 
    150     if (readyState != 1) 
     126    if (readyState > 1) 
    151127      this.respondToReadyState(this.transport.readyState); 
    152128  }, 
    153129   
    154   header: function(name) { 
    155     try { 
    156       return this.transport.getResponseHeader(name); 
    157     } catch (e) {} 
    158   }, 
    159    
    160   evalJSON: function() { 
    161     try { 
    162       return eval('(' + this.header('X-JSON') + ')'); 
    163     } catch (e) {} 
    164   }, 
    165    
    166   evalResponse: function() { 
    167     try { 
    168       return eval(this.transport.responseText); 
    169     } catch (e) { 
    170       this.dispatchException(e); 
    171     } 
     130  setRequestHeaders: function() { 
     131    var headers = { 
     132      'X-Requested-With': 'XMLHttpRequest', 
     133      'X-Prototype-Version': Prototype.Version, 
     134      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' 
     135    }; 
     136 
     137    if (this.options.method == 'post') { 
     138      headers['Content-type'] = this.options.contentType + 
     139        (this.options.encoding ? '; charset=' + this.options.encoding : ''); 
     140       
     141      /* Force "Connection: close" for older Mozilla browsers to work 
     142       * around a bug where XMLHttpRequest sends an incorrect 
     143       * Content-length header. See Mozilla Bugzilla #246651.  
     144       */ 
     145      if (this.transport.overrideMimeType && 
     146          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) 
     147            headers['Connection'] = 'close'; 
     148    } 
     149     
     150    // user-defined headers 
     151    if (typeof this.options.requestHeaders == 'object') { 
     152      var extras = this.options.requestHeaders; 
     153 
     154      if (typeof extras.push == 'function') 
     155        for (var i = 0; i < extras.length; i += 2)  
     156          headers[extras[i]] = extras[i+1]; 
     157      else 
     158        $H(extras).each(function(pair) { headers[pair.key] = pair.value }); 
     159    } 
     160 
     161    for (var name in headers)  
     162      this.transport.setRequestHeader(name, headers[name]); 
     163  }, 
     164   
     165  success: function() { 
     166    return !this.transport.status 
     167        || (this.transport.status >= 200 && this.transport.status < 300); 
    172168  }, 
    173169 
    174170  respondToReadyState: function(readyState) { 
    175     var event = Ajax.Request.Events[readyState]; 
     171    var state = Ajax.Request.Events[readyState]; 
    176172    var transport = this.transport, json = this.evalJSON(); 
    177173 
    178     if (event == 'Complete') { 
     174    if (state == 'Complete') { 
    179175      try { 
    180176        (this.options['on' + this.transport.status] 
    181          || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] 
     177         || this.options['on' + (this.success() ? 'Success' : 'Failure')] 
    182178         || Prototype.emptyFunction)(transport, json); 
    183179      } catch (e) { 
    184180        this.dispatchException(e); 
    185181      } 
    186        
    187       if ((this.header('Content-type') || '').match(/^text\/javascript/i)) 
    188         this.evalResponse(); 
    189     } 
    190      
    191     try { 
    192       (this.options['on' + event] || Prototype.emptyFunction)(transport, json); 
    193       Ajax.Responders.dispatch('on' + event, this, transport, json); 
     182    } 
     183 
     184    try { 
     185      (this.options['on' + state] || Prototype.emptyFunction)(transport, json); 
     186      Ajax.Responders.dispatch('on' + state, this, transport, json); 
    194187    } catch (e) { 
    195188      this.dispatchException(e); 
    196189    } 
    197190     
    198     /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ 
    199     if (event == 'Complete') 
     191    if (state == 'Complete') { 
     192      if ((this.getHeader('Content-type') || '').strip(). 
     193        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i)) 
     194          this.evalResponse(); 
     195       
     196      // avoid memory leak in MSIE: clean up 
    200197      this.transport.onreadystatechange = Prototype.emptyFunction; 
    201   }, 
    202    
     198    } 
     199  }, 
     200   
     201  getHeader: function(name) { 
     202    try { 
     203      return this.transport.getResponseHeader(name); 
     204    } catch (e) { return null } 
     205  }, 
     206   
     207  evalJSON: function() { 
     208    try { 
     209      var json = this.getHeader('X-JSON'); 
     210      return json ? eval('(' + json + ')') : null; 
     211    } catch (e) { return null } 
     212  }, 
     213   
     214  evalResponse: function() { 
     215    try { 
     216      return eval(this.transport.responseText); 
     217    } catch (e) { 
     218      this.dispatchException(e); 
     219    } 
     220  }, 
     221 
    203222  dispatchException: function(exception) { 
    204223    (this.options.onException || Prototype.emptyFunction)(this, exception); 
     
    211230Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { 
    212231  initialize: function(container, url, options) { 
    213     this.containers = { 
    214       success: container.success ? $(container.success) : $(container), 
    215       failure: container.failure ? $(container.failure) : 
    216         (container.success ? null : $(container)) 
    217     } 
    218  
     232    this.container = { 
     233      success: (container.success || container), 
     234      failure: (container.failure || (container.success ? null : container)) 
     235    } 
     236     
    219237    this.transport = Ajax.getTransport(); 
    220238    this.setOptions(options); 
    221239 
    222240    var onComplete = this.options.onComplete || Prototype.emptyFunction; 
    223     this.options.onComplete = (function(transport, object) { 
     241    this.options.onComplete = (function(transport, param) { 
    224242      this.updateContent(); 
    225       onComplete(transport, object); 
     243      onComplete(transport, param); 
    226244    }).bind(this); 
    227245 
     
    230248 
    231249  updateContent: function() { 
    232     var receiver = this.responseIsSuccess() ? 
    233       this.containers.success : this.containers.failure; 
     250    var receiver = this.container[this.success() ? 'success' : 'failure']; 
    234251    var response = this.transport.responseText; 
    235252     
    236     if (!this.options.evalScripts) 
    237       response = response.stripScripts(); 
    238  
    239     if (receiver) { 
    240       if (this.options.insertion) { 
     253    if (!this.options.evalScripts) response = response.stripScripts(); 
     254     
     255    if (receiver = $(receiver)) { 
     256      if (this.options.insertion) 
    241257        new this.options.insertion(receiver, response); 
    242       } else { 
    243         Element.update(receiver, response); 
    244       } 
    245     } 
    246  
    247     if (this.responseIsSuccess()) { 
     258      else 
     259        receiver.update(response); 
     260    } 
     261     
     262    if (this.success()) { 
    248263      if (this.onComplete) 
    249264        setTimeout(this.onComplete.bind(this), 10); 
  • spinoffs/prototype/src/hash.js

    r4181 r5448  
    2121   
    2222  merge: function(hash) { 
    23     return $H(hash).inject($H(this), function(mergedHash, pair) { 
     23    return $H(hash).inject(this, function(mergedHash, pair) { 
    2424      mergedHash[pair.key] = pair.value; 
    2525      return mergedHash; 
     
    2929  toQueryString: function() { 
    3030    return this.map(function(pair) { 
     31      if (!pair.value && pair.value !== 0) pair[1] = ''; 
     32      if (!pair.key) return; 
    3133      return pair.map(encodeURIComponent).join('='); 
    3234    }).join('&'); 
  • spinoffs/prototype/src/string.js

    r4986 r5448  
    7676   
    7777  toQueryParams: function() { 
    78     var pairs = this.match(/^\??(.*)$/)[1].split('&'); 
     78    var match = this.strip().match(/[^?]*$/)[0]; 
     79    if (!match) return {}; 
     80    var pairs = match.split('&'); 
    7981    return pairs.inject({}, function(params, pairString) { 
    8082      var pair  = pairString.split('='); 
  • spinoffs/prototype/test/unit/ajax.html

    r5142 r5448  
    2424<div id="testlog"> </div> 
    2525<div id="content"></div> 
     26<div id="content2" style="color:red"></div> 
    2627 
    2728<!-- Tests follow --> 
     
    3132     
    3233    setup: function(){ 
    33       $('content').innerHTML = ''; 
     34      $('content').update(''); 
     35      $('content2').update(''); 
    3436    }, 
    3537     
     
    3840      new Ajax.Request("fixtures/hello.js", { 
    3941        asynchronous: false, 
    40         method: 'get', 
     42        method: 'GET', 
    4143        onComplete: function(response) { eval(response.responseText) } 
    4244      }); 
     
    4850    testAsynchronousRequest: function() {with(this) { 
    4951      assertEqual("", $("content").innerHTML); 
     52       
    5053      new Ajax.Request("fixtures/hello.js", { 
    5154        asynchronous: true, 
     
    5760        assertEqual("Hello world!", h2.innerHTML); 
    5861      }); 
     62    }}, 
     63     
     64    testUpdater: function() {with(this) { 
     65      assertEqual("", $("content").innerHTML); 
     66       
     67      new Ajax.Updater("content", "fixtures/content.html", { method:'get' }); 
     68       
     69      // lowercase comparison because of MSIE which presents HTML tags in uppercase 
     70      var sentence = ("Pack my box with <em>five dozen</em> liquor jugs! " + 
     71        "Oh, how <strong>quickly</strong> daft jumping zebras vex...").toLowerCase(); 
     72 
     73      wait(1000,function(){ 
     74        assertEqual(sentence, $("content").innerHTML.strip().toLowerCase()); 
     75         
     76        $('content').update(''); 
     77        assertEqual("", $("content").innerHTML); 
     78          
     79        Ajax.Responders.register({onComplete: function(req){ 
     80          assertEqual("fixtures/content.html?pet=monkey&_=", req.url); 
     81        }.bind(this) }); 
     82         
     83        new Ajax.Updater({ success:"content", failure:"content2" }, 
     84          "fixtures/content.html", { method:'get', parameters:{ pet:'monkey' } }); 
     85         
     86        new Ajax.Updater("", "fixtures/content.html", { method:'get', parameters:"pet=monkey" }); 
     87         
     88        wait(1000,function(){ 
     89          assertEqual(sentence, $("content").innerHTML.strip().toLowerCase()); 
     90          assertEqual("", $("content2").innerHTML); 
     91        }); 
     92      }); 
    5993    }} 
    6094     
  • spinoffs/prototype/test/unit/hash.html

    r5295 r5448  
    3838      c: 'C', 
    3939      d: 'D#' 
    40     }     
     40    }, 
     41     
     42    value_undefined: { a:"b", c:undefined }, 
     43    value_null: { a:"b", c:null }, 
     44    value_zero: { a:"b", c:0 } 
    4145  }; 
     46   
    4247  new Test.Unit.Runner({ 
    4348     
     
    6570      assertEqual('a=A%23',                  $H(Fixtures.one).toQueryString()) 
    6671      assertEqual('a=A&b=B&c=C&d=D%23',      $H(Fixtures.many).toQueryString()) 
     72      assertEqual("a=b&c=",                  $H(Fixtures.value_undefined).toQueryString()) 
     73      assertEqual("a=b&c=",                  $H(Fixtures.value_null).toQueryString()) 
     74      assertEqual("a=b&c=0",                 $H(Fixtures.value_zero).toQueryString()) 
    6775    }}, 
    6876     
  • spinoffs/prototype/test/unit/string.html

    r5141 r5448  
    207207     
    208208    testToQueryParams: function() {with(this) { 
    209       assertEnumEqual([''], Object.keys(''.toQueryParams())); 
     209      assertEnumEqual([], Object.keys(''.toQueryParams())); 
     210      assertEnumEqual([], Object.keys('foo?'.toQueryParams())); 
     211      assertEnumEqual(['a', 'b'], Object.keys('foo?a&b'.toQueryParams())); 
    210212       
    211213      var result = 'a'.toQueryParams();