523 lines
15 KiB
JavaScript
523 lines
15 KiB
JavaScript
|
/*!
|
||
|
* Reqwest! A general purpose XHR connection manager
|
||
|
* (c) Dustin Diaz 2012
|
||
|
* https://github.com/ded/reqwest
|
||
|
* license MIT
|
||
|
*/
|
||
|
;(function (name, context, definition) {
|
||
|
if (typeof module != 'undefined' && module.exports) module.exports = definition()
|
||
|
else if (typeof define == 'function' && define.amd) define(definition)
|
||
|
else context[name] = definition()
|
||
|
})('reqwest', this, function () {
|
||
|
|
||
|
var win = window
|
||
|
, doc = document
|
||
|
, twoHundo = /^20\d$/
|
||
|
, byTag = 'getElementsByTagName'
|
||
|
, readyState = 'readyState'
|
||
|
, contentType = 'Content-Type'
|
||
|
, requestedWith = 'X-Requested-With'
|
||
|
, head = doc[byTag]('head')[0]
|
||
|
, uniqid = 0
|
||
|
, callbackPrefix = 'reqwest_' + (+new Date())
|
||
|
, lastValue // data stored by the most recent JSONP callback
|
||
|
, xmlHttpRequest = 'XMLHttpRequest'
|
||
|
, noop = function () {}
|
||
|
|
||
|
, isArray = typeof Array.isArray == 'function'
|
||
|
? Array.isArray
|
||
|
: function (a) {
|
||
|
return a instanceof Array
|
||
|
}
|
||
|
|
||
|
, defaultHeaders = {
|
||
|
contentType: 'application/x-www-form-urlencoded'
|
||
|
, requestedWith: xmlHttpRequest
|
||
|
, accept: {
|
||
|
'*': 'text/javascript, text/html, application/xml, text/xml, */*'
|
||
|
, xml: 'application/xml, text/xml'
|
||
|
, html: 'text/html'
|
||
|
, text: 'text/plain'
|
||
|
, json: 'application/json, text/javascript'
|
||
|
, js: 'application/javascript, text/javascript'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
, xhr = win[xmlHttpRequest]
|
||
|
? function () {
|
||
|
return new XMLHttpRequest()
|
||
|
}
|
||
|
: function () {
|
||
|
return new ActiveXObject('Microsoft.XMLHTTP')
|
||
|
}
|
||
|
|
||
|
function handleReadyState (r, success, error) {
|
||
|
return function () {
|
||
|
// use _aborted to mitigate against IE err c00c023f
|
||
|
// (can't read props on aborted request objects)
|
||
|
if (r._aborted) return error(r.request)
|
||
|
if (r.request && r.request[readyState] == 4) {
|
||
|
r.request.onreadystatechange = noop
|
||
|
if (twoHundo.test(r.request.status))
|
||
|
success(r.request)
|
||
|
else
|
||
|
error(r.request)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function setHeaders (http, o) {
|
||
|
var headers = o.headers || {}
|
||
|
, h
|
||
|
|
||
|
headers.Accept = headers.Accept
|
||
|
|| defaultHeaders.accept[o.type]
|
||
|
|| defaultHeaders.accept['*']
|
||
|
|
||
|
// breaks cross-origin requests with legacy browsers
|
||
|
if (!o.crossOrigin && !headers[requestedWith]) headers[requestedWith] = defaultHeaders.requestedWith
|
||
|
if (!headers[contentType]) headers[contentType] = o.contentType || defaultHeaders.contentType
|
||
|
for (h in headers)
|
||
|
headers.hasOwnProperty(h) && http.setRequestHeader(h, headers[h])
|
||
|
}
|
||
|
|
||
|
function setCredentials (http, o) {
|
||
|
if (typeof o.withCredentials !== 'undefined' && typeof http.withCredentials !== 'undefined') {
|
||
|
http.withCredentials = !!o.withCredentials
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function generalCallback (data) {
|
||
|
lastValue = data
|
||
|
}
|
||
|
|
||
|
function urlappend (url, s) {
|
||
|
return url + (/\?/.test(url) ? '&' : '?') + s
|
||
|
}
|
||
|
|
||
|
function handleJsonp (o, fn, err, url) {
|
||
|
var reqId = uniqid++
|
||
|
, cbkey = o.jsonpCallback || 'callback' // the 'callback' key
|
||
|
, cbval = o.jsonpCallbackName || reqwest.getcallbackPrefix(reqId)
|
||
|
// , cbval = o.jsonpCallbackName || ('reqwest_' + reqId) // the 'callback' value
|
||
|
, cbreg = new RegExp('((^|\\?|&)' + cbkey + ')=([^&]+)')
|
||
|
, match = url.match(cbreg)
|
||
|
, script = doc.createElement('script')
|
||
|
, loaded = 0
|
||
|
, isIE10 = navigator.userAgent.indexOf('MSIE 10.0') !== -1
|
||
|
, isIE9 = navigator.userAgent.indexOf('MSIE 9.0') !== -1;
|
||
|
|
||
|
if (match) {
|
||
|
if (match[3] === '?') {
|
||
|
url = url.replace(cbreg, '$1=' + cbval) // wildcard callback func name
|
||
|
} else {
|
||
|
cbval = match[3] // provided callback func name
|
||
|
}
|
||
|
} else {
|
||
|
url = urlappend(url, cbkey + '=' + cbval) // no callback details, add 'em
|
||
|
}
|
||
|
|
||
|
win[cbval] = generalCallback
|
||
|
|
||
|
script.type = 'text/javascript'
|
||
|
script.src = url
|
||
|
script.async = true
|
||
|
if (typeof script.onreadystatechange !== 'undefined' && !isIE10 && !isIE9) {
|
||
|
// need this for IE due to out-of-order onreadystatechange(), binding script
|
||
|
// execution to an event listener gives us control over when the script
|
||
|
// is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
|
||
|
//
|
||
|
// if this hack is used in IE10 jsonp callback are never called
|
||
|
script.event = 'onclick'
|
||
|
script.htmlFor = script.id = '_reqwest_' + reqId
|
||
|
}
|
||
|
|
||
|
script.onload = script.onreadystatechange = function () {
|
||
|
if ((script[readyState] && script[readyState] !== 'complete' && script[readyState] !== 'loaded') || loaded) {
|
||
|
return false
|
||
|
}
|
||
|
script.onload = script.onreadystatechange = null
|
||
|
script.onclick && script.onclick()
|
||
|
// Call the user callback with the last value stored and clean up values and scripts.
|
||
|
o.success && o.success(lastValue)
|
||
|
lastValue = undefined
|
||
|
head.removeChild(script)
|
||
|
loaded = 1
|
||
|
}
|
||
|
|
||
|
// Add the script to the DOM head
|
||
|
head.appendChild(script)
|
||
|
|
||
|
// Enable JSONP timeout
|
||
|
return {
|
||
|
abort: function () {
|
||
|
script.onload = script.onreadystatechange = null
|
||
|
o.error && o.error({}, 'Request is aborted: timeout', {})
|
||
|
lastValue = undefined
|
||
|
head.removeChild(script)
|
||
|
loaded = 1
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getRequest (fn, err) {
|
||
|
var o = this.o
|
||
|
, method = (o.method || 'GET').toUpperCase()
|
||
|
, url = typeof o === 'string' ? o : o.url
|
||
|
// convert non-string objects to query-string form unless o.processData is false
|
||
|
, data = (o.processData !== false && o.data && typeof o.data !== 'string')
|
||
|
? reqwest.toQueryString(o.data)
|
||
|
: (o.data || null)
|
||
|
, http
|
||
|
|
||
|
// if we're working on a GET request and we have data then we should append
|
||
|
// query string to end of URL and not post data
|
||
|
if ((o.type == 'jsonp' || method == 'GET') && data) {
|
||
|
url = urlappend(url, data)
|
||
|
data = null
|
||
|
}
|
||
|
|
||
|
if (o.type == 'jsonp') return handleJsonp(o, fn, err, url)
|
||
|
|
||
|
http = xhr()
|
||
|
http.open(method, url, true)
|
||
|
setHeaders(http, o)
|
||
|
setCredentials(http, o)
|
||
|
http.onreadystatechange = handleReadyState(this, fn, err)
|
||
|
o.before && o.before(http)
|
||
|
http.send(data)
|
||
|
return http
|
||
|
}
|
||
|
|
||
|
function Reqwest (o, fn) {
|
||
|
this.o = o
|
||
|
this.fn = fn
|
||
|
|
||
|
init.apply(this, arguments)
|
||
|
}
|
||
|
|
||
|
function setType (url) {
|
||
|
var m = url.match(/\.(json|jsonp|html|xml)(\?|$)/)
|
||
|
return m ? m[1] : 'js'
|
||
|
}
|
||
|
|
||
|
function init (o, fn) {
|
||
|
|
||
|
this.url = typeof o == 'string' ? o : o.url
|
||
|
this.timeout = null
|
||
|
|
||
|
// whether request has been fulfilled for purpose
|
||
|
// of tracking the Promises
|
||
|
this._fulfilled = false
|
||
|
// success handlers
|
||
|
this._fulfillmentHandlers = []
|
||
|
// error handlers
|
||
|
this._errorHandlers = []
|
||
|
// complete (both success and fail) handlers
|
||
|
this._completeHandlers = []
|
||
|
this._erred = false
|
||
|
this._responseArgs = {}
|
||
|
|
||
|
var self = this
|
||
|
, type = o.type || setType(this.url)
|
||
|
|
||
|
fn = fn || function () {}
|
||
|
|
||
|
if (o.timeout) {
|
||
|
this.timeout = setTimeout(function () {
|
||
|
self.abort()
|
||
|
}, o.timeout)
|
||
|
}
|
||
|
|
||
|
if (o.success) {
|
||
|
this._fulfillmentHandlers.push(function () {
|
||
|
o.success.apply(o, arguments)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if (o.error) {
|
||
|
this._errorHandlers.push(function () {
|
||
|
o.error.apply(o, arguments)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if (o.complete) {
|
||
|
this._completeHandlers.push(function () {
|
||
|
o.complete.apply(o, arguments)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function complete (resp) {
|
||
|
o.timeout && clearTimeout(self.timeout)
|
||
|
self.timeout = null
|
||
|
while (self._completeHandlers.length > 0) {
|
||
|
self._completeHandlers.shift()(resp)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function success (resp) {
|
||
|
var r = resp.responseText
|
||
|
if (r) {
|
||
|
switch (type) {
|
||
|
case 'json':
|
||
|
try {
|
||
|
resp = win.JSON ? win.JSON.parse(r) : eval('(' + r + ')')
|
||
|
} catch (err) {
|
||
|
return error(resp, 'Could not parse JSON in response', err)
|
||
|
}
|
||
|
break
|
||
|
case 'js':
|
||
|
resp = eval(r)
|
||
|
break
|
||
|
case 'html':
|
||
|
resp = r
|
||
|
break
|
||
|
case 'xml':
|
||
|
resp = resp.responseXML
|
||
|
&& resp.responseXML.parseError // IE trololo
|
||
|
&& resp.responseXML.parseError.errorCode
|
||
|
&& resp.responseXML.parseError.reason
|
||
|
? null
|
||
|
: resp.responseXML
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
self._responseArgs.resp = resp
|
||
|
self._fulfilled = true
|
||
|
fn(resp)
|
||
|
while (self._fulfillmentHandlers.length > 0) {
|
||
|
self._fulfillmentHandlers.shift()(resp)
|
||
|
}
|
||
|
|
||
|
complete(resp)
|
||
|
}
|
||
|
|
||
|
function error (resp, msg, t) {
|
||
|
self._responseArgs.resp = resp
|
||
|
self._responseArgs.msg = msg
|
||
|
self._responseArgs.t = t
|
||
|
self._erred = true
|
||
|
while (self._errorHandlers.length > 0) {
|
||
|
self._errorHandlers.shift()(resp, msg, t)
|
||
|
}
|
||
|
complete(resp)
|
||
|
}
|
||
|
|
||
|
this.request = getRequest.call(this, success, error)
|
||
|
}
|
||
|
|
||
|
Reqwest.prototype = {
|
||
|
abort: function () {
|
||
|
this._aborted = true
|
||
|
this.request.abort()
|
||
|
}
|
||
|
|
||
|
, retry: function () {
|
||
|
init.call(this, this.o, this.fn)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Small deviation from the Promises A CommonJs specification
|
||
|
* http://wiki.commonjs.org/wiki/Promises/A
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* `then` will execute upon successful requests
|
||
|
*/
|
||
|
, then: function (success, fail) {
|
||
|
if (this._fulfilled) {
|
||
|
success(this._responseArgs.resp)
|
||
|
} else if (this._erred) {
|
||
|
fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
|
||
|
} else {
|
||
|
this._fulfillmentHandlers.push(success)
|
||
|
this._errorHandlers.push(fail)
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* `always` will execute whether the request succeeds or fails
|
||
|
*/
|
||
|
, always: function (fn) {
|
||
|
if (this._fulfilled || this._erred) {
|
||
|
fn(this._responseArgs.resp)
|
||
|
} else {
|
||
|
this._completeHandlers.push(fn)
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* `fail` will execute when the request fails
|
||
|
*/
|
||
|
, fail: function (fn) {
|
||
|
if (this._erred) {
|
||
|
fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
|
||
|
} else {
|
||
|
this._errorHandlers.push(fn)
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function reqwest (o, fn) {
|
||
|
return new Reqwest(o, fn)
|
||
|
}
|
||
|
|
||
|
// normalize newline variants according to spec -> CRLF
|
||
|
function normalize (s) {
|
||
|
return s ? s.replace(/\r?\n/g, '\r\n') : ''
|
||
|
}
|
||
|
|
||
|
function serial (el, cb) {
|
||
|
var n = el.name
|
||
|
, t = el.tagName.toLowerCase()
|
||
|
, optCb = function (o) {
|
||
|
// IE gives value="" even where there is no value attribute
|
||
|
// 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273
|
||
|
if (o && !o.disabled)
|
||
|
cb(n, normalize(o.attributes.value && o.attributes.value.specified ? o.value : o.text))
|
||
|
}
|
||
|
, ch, ra, val, i
|
||
|
|
||
|
// don't serialize elements that are disabled or without a name
|
||
|
if (el.disabled || !n) return
|
||
|
|
||
|
switch (t) {
|
||
|
case 'input':
|
||
|
if (!/reset|button|image|file/i.test(el.type)) {
|
||
|
ch = /checkbox/i.test(el.type)
|
||
|
ra = /radio/i.test(el.type)
|
||
|
val = el.value
|
||
|
// WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here
|
||
|
;(!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val))
|
||
|
}
|
||
|
break
|
||
|
case 'textarea':
|
||
|
cb(n, normalize(el.value))
|
||
|
break
|
||
|
case 'select':
|
||
|
if (el.type.toLowerCase() === 'select-one') {
|
||
|
optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null)
|
||
|
} else {
|
||
|
for (i = 0; el.length && i < el.length; i++) {
|
||
|
el.options[i].selected && optCb(el.options[i])
|
||
|
}
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// collect up all form elements found from the passed argument elements all
|
||
|
// the way down to child elements; pass a '<form>' or form fields.
|
||
|
// called with 'this'=callback to use for serial() on each element
|
||
|
function eachFormElement () {
|
||
|
var cb = this
|
||
|
, e, i
|
||
|
, serializeSubtags = function (e, tags) {
|
||
|
var i, j, fa
|
||
|
for (i = 0; i < tags.length; i++) {
|
||
|
fa = e[byTag](tags[i])
|
||
|
for (j = 0; j < fa.length; j++) serial(fa[j], cb)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < arguments.length; i++) {
|
||
|
e = arguments[i]
|
||
|
if (/input|select|textarea/i.test(e.tagName)) serial(e, cb)
|
||
|
serializeSubtags(e, [ 'input', 'select', 'textarea' ])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// standard query string style serialization
|
||
|
function serializeQueryString () {
|
||
|
return reqwest.toQueryString(reqwest.serializeArray.apply(null, arguments))
|
||
|
}
|
||
|
|
||
|
// { 'name': 'value', ... } style serialization
|
||
|
function serializeHash () {
|
||
|
var hash = {}
|
||
|
eachFormElement.apply(function (name, value) {
|
||
|
if (name in hash) {
|
||
|
hash[name] && !isArray(hash[name]) && (hash[name] = [hash[name]])
|
||
|
hash[name].push(value)
|
||
|
} else hash[name] = value
|
||
|
}, arguments)
|
||
|
return hash
|
||
|
}
|
||
|
|
||
|
// [ { name: 'name', value: 'value' }, ... ] style serialization
|
||
|
reqwest.serializeArray = function () {
|
||
|
var arr = []
|
||
|
eachFormElement.apply(function (name, value) {
|
||
|
arr.push({name: name, value: value})
|
||
|
}, arguments)
|
||
|
return arr
|
||
|
}
|
||
|
|
||
|
reqwest.serialize = function () {
|
||
|
if (arguments.length === 0) return ''
|
||
|
var opt, fn
|
||
|
, args = Array.prototype.slice.call(arguments, 0)
|
||
|
|
||
|
opt = args.pop()
|
||
|
opt && opt.nodeType && args.push(opt) && (opt = null)
|
||
|
opt && (opt = opt.type)
|
||
|
|
||
|
if (opt == 'map') fn = serializeHash
|
||
|
else if (opt == 'array') fn = reqwest.serializeArray
|
||
|
else fn = serializeQueryString
|
||
|
|
||
|
return fn.apply(null, args)
|
||
|
}
|
||
|
|
||
|
reqwest.toQueryString = function (o) {
|
||
|
var qs = '', i
|
||
|
, enc = encodeURIComponent
|
||
|
, push = function (k, v) {
|
||
|
qs += enc(k) + '=' + enc(v) + '&'
|
||
|
}
|
||
|
, k, v
|
||
|
|
||
|
if (isArray(o)) {
|
||
|
for (i = 0; o && i < o.length; i++) push(o[i].name, o[i].value)
|
||
|
} else {
|
||
|
for (k in o) {
|
||
|
if (!Object.hasOwnProperty.call(o, k)) continue
|
||
|
v = o[k]
|
||
|
if (isArray(v)) {
|
||
|
for (i = 0; i < v.length; i++) push(k, v[i])
|
||
|
} else push(k, o[k])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// spaces should be + according to spec
|
||
|
return qs.replace(/&$/, '').replace(/%20/g, '+')
|
||
|
}
|
||
|
|
||
|
reqwest.getcallbackPrefix = function () {
|
||
|
return callbackPrefix
|
||
|
}
|
||
|
|
||
|
// evaluates f if it's a function or returns the actual value
|
||
|
function getValue(f) {
|
||
|
return typeof(f) === 'function' ? f(): f;
|
||
|
}
|
||
|
|
||
|
// jQuery and Zepto compatibility, differences can be remapped here so you can call
|
||
|
// .ajax.compat(options, callback)
|
||
|
reqwest.compat = function (o, fn) {
|
||
|
if (o) {
|
||
|
o.type && (o.method = o.type) && delete o.type
|
||
|
o.dataType && (o.type = o.dataType)
|
||
|
o.jsonpCallback && (o.jsonpCallbackName = getValue(o.jsonpCallback)) && delete o.jsonpCallback
|
||
|
o.jsonp && (o.jsonpCallback = o.jsonp)
|
||
|
}
|
||
|
return new Reqwest(o, fn)
|
||
|
}
|
||
|
|
||
|
return reqwest
|
||
|
})
|