Changeset 5448
- Timestamp:
- 11/07/06 06:23:31 (2 years ago)
- Files:
-
- spinoffs/prototype/CHANGELOG (modified) (1 diff)
- spinoffs/prototype/src/ajax.js (modified) (6 diffs)
- spinoffs/prototype/src/hash.js (modified) (2 diffs)
- spinoffs/prototype/src/string.js (modified) (1 diff)
- spinoffs/prototype/test/unit/ajax.html (modified) (5 diffs)
- spinoffs/prototype/test/unit/fixtures/content.html (added)
- spinoffs/prototype/test/unit/hash.html (modified) (2 diffs)
- spinoffs/prototype/test/unit/string.html (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
spinoffs/prototype/CHANGELOG
r5447 r5448 1 1 *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 2 14 3 15 * 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 18 18 }, 19 19 20 register: function(responder ToAdd) {21 if (!this.include(responder ToAdd))22 this.responders.push(responder ToAdd);23 }, 24 25 unregister: function(responder ToRemove) {26 this.responders = this.responders.without(responder ToRemove);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); 27 27 }, 28 28 29 29 dispatch: function(callback, request, transport, json) { 30 30 this.each(function(responder) { 31 if ( responder[callback] &&typeof responder[callback] == 'function') {31 if (typeof responder[callback] == 'function') { 32 32 try { 33 33 responder[callback].apply(responder, [request, transport, json]); … … 43 43 onCreate: function() { 44 44 Ajax.activeRequestCount++; 45 }, 46 45 }, 47 46 onComplete: function() { 48 47 Ajax.activeRequestCount--; … … 57 56 asynchronous: true, 58 57 contentType: 'application/x-www-form-urlencoded', 58 encoding: 'UTF-8', 59 59 parameters: '' 60 60 } 61 61 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 } 73 67 } 74 68 … … 85 79 86 80 request: function(url) { 87 var param eters = this.options.parameters || '';88 if (param eters.length > 0) parameters += '&_=';89 90 /* Simulate other verbs over post */91 if (this.options.method != 'get' && this.options.method != 'post') {92 param eters += (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; 93 87 this.options.method = 'post'; 94 88 } 95 89 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 { 101 98 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); 105 103 106 104 if (this.options.asynchronous) 107 105 setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10); 108 106 109 107 this.transport.onreadystatechange = this.onStateChange.bind(this); 110 108 this.setRequestHeaders(); 111 109 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); 114 114 115 115 /* Force Firefox to handle ready state 4 for synchronous requests */ 116 116 if (!this.options.asynchronous && this.transport.overrideMimeType) 117 117 this.onStateChange(); 118 119 }catch (e) {118 } 119 catch (e) { 120 120 this.dispatchException(e); 121 121 } 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 around134 * a bug where XMLHttpReqeuest sends an incorrect Content-length135 * 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]);146 122 }, 147 123 148 124 onStateChange: function() { 149 125 var readyState = this.transport.readyState; 150 if (readyState !=1)126 if (readyState > 1) 151 127 this.respondToReadyState(this.transport.readyState); 152 128 }, 153 129 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); 172 168 }, 173 169 174 170 respondToReadyState: function(readyState) { 175 var event= Ajax.Request.Events[readyState];171 var state = Ajax.Request.Events[readyState]; 176 172 var transport = this.transport, json = this.evalJSON(); 177 173 178 if ( event== 'Complete') {174 if (state == 'Complete') { 179 175 try { 180 176 (this.options['on' + this.transport.status] 181 || this.options['on' + (this. responseIsSuccess() ? 'Success' : 'Failure')]177 || this.options['on' + (this.success() ? 'Success' : 'Failure')] 182 178 || Prototype.emptyFunction)(transport, json); 183 179 } catch (e) { 184 180 this.dispatchException(e); 185 181 } 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); 194 187 } catch (e) { 195 188 this.dispatchException(e); 196 189 } 197 190 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 200 197 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 203 222 dispatchException: function(exception) { 204 223 (this.options.onException || Prototype.emptyFunction)(this, exception); … … 211 230 Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { 212 231 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 219 237 this.transport = Ajax.getTransport(); 220 238 this.setOptions(options); 221 239 222 240 var onComplete = this.options.onComplete || Prototype.emptyFunction; 223 this.options.onComplete = (function(transport, object) {241 this.options.onComplete = (function(transport, param) { 224 242 this.updateContent(); 225 onComplete(transport, object);243 onComplete(transport, param); 226 244 }).bind(this); 227 245 … … 230 248 231 249 updateContent: function() { 232 var receiver = this.responseIsSuccess() ? 233 this.containers.success : this.containers.failure; 250 var receiver = this.container[this.success() ? 'success' : 'failure']; 234 251 var response = this.transport.responseText; 235 252 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) 241 257 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()) { 248 263 if (this.onComplete) 249 264 setTimeout(this.onComplete.bind(this), 10); spinoffs/prototype/src/hash.js
r4181 r5448 21 21 22 22 merge: function(hash) { 23 return $H(hash).inject( $H(this), function(mergedHash, pair) {23 return $H(hash).inject(this, function(mergedHash, pair) { 24 24 mergedHash[pair.key] = pair.value; 25 25 return mergedHash; … … 29 29 toQueryString: function() { 30 30 return this.map(function(pair) { 31 if (!pair.value && pair.value !== 0) pair[1] = ''; 32 if (!pair.key) return; 31 33 return pair.map(encodeURIComponent).join('='); 32 34 }).join('&'); spinoffs/prototype/src/string.js
r4986 r5448 76 76 77 77 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('&'); 79 81 return pairs.inject({}, function(params, pairString) { 80 82 var pair = pairString.split('='); spinoffs/prototype/test/unit/ajax.html
r5142 r5448 24 24 <div id="testlog"> </div> 25 25 <div id="content"></div> 26 <div id="content2" style="color:red"></div> 26 27 27 28 <!-- Tests follow --> … … 31 32 32 33 setup: function(){ 33 $('content').innerHTML = ''; 34 $('content').update(''); 35 $('content2').update(''); 34 36 }, 35 37 … … 38 40 new Ajax.Request("fixtures/hello.js", { 39 41 asynchronous: false, 40 method: ' get',42 method: 'GET', 41 43 onComplete: function(response) { eval(response.responseText) } 42 44 }); … … 48 50 testAsynchronousRequest: function() {with(this) { 49 51 assertEqual("", $("content").innerHTML); 52 50 53 new Ajax.Request("fixtures/hello.js", { 51 54 asynchronous: true, … … 57 60 assertEqual("Hello world!", h2.innerHTML); 58 61 }); 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 }); 59 93 }} 60 94 spinoffs/prototype/test/unit/hash.html
r5295 r5448 38 38 c: 'C', 39 39 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 } 41 45 }; 46 42 47 new Test.Unit.Runner({ 43 48 … … 65 70 assertEqual('a=A%23', $H(Fixtures.one).toQueryString()) 66 71 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()) 67 75 }}, 68 76 spinoffs/prototype/test/unit/string.html
r5141 r5448 207 207 208 208 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())); 210 212 211 213 var result = 'a'.toQueryParams();