├── .gitignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── dist
├── gumshoe.js
└── gumshoe.min.js
├── examples
└── transports
│ └── transport-example.js
├── gulpfile.js
├── lib
├── _polyfills.js
├── perfnow.js
├── query-string.js
├── reqwest.js
└── store2.js
├── logo.png
├── package.json
├── src
└── gumshoe.js
└── test
├── fixtures.js
├── promise
├── rsvp-runner.html
└── rsvp-specs.js
├── runner-dist.html
├── runner.html
├── sessions
├── sessions-runner.html
└── sessions-specs.js
└── specs.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "expr": true,
4 | "newcap": true,
5 | "quotmark": "single",
6 | "regexdash": true,
7 | "trailing": true,
8 | "undef": true,
9 | "unused": false,
10 | "maxerr": 100,
11 | "eqnull": true,
12 | "sub": false,
13 | "browser": true,
14 | "node": true,
15 | "esnext": true,
16 | "globals": {
17 | "createModule": false,
18 | "requireModules": false,
19 | "requireSpecs": false,
20 | "getJSONString": false,
21 | "describe": false,
22 | "xdescribe": false,
23 | "it": false,
24 | "xit": false,
25 | "expect": false,
26 | "jasmine": false,
27 | "spyOn": false,
28 | "afterEach": false,
29 | "beforeEach": false,
30 | "waits": false,
31 | "waitsFor": false,
32 | "runs": false,
33 | "sinon": false
34 | }
35 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | .jshintrc
3 | .npmignore
4 | .travis.yml
5 | gulpfile.js
6 | test/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - '7.6'
5 |
6 | before_install:
7 | - npm install -g gulp --loglevel=silent
8 | - echo `realpath node_modules`
9 |
10 | script:
11 | - gulp test
12 |
13 | matrix:
14 | fast_finish: true
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Gilt Groupe, Inc., Andrew Powell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gumshoe [](https://travis-ci.org/gumshoe/Gumshoe)
2 |
3 |
4 |
5 | An analytics and event tracking sleuth.
6 |
7 | ## Background
8 |
9 | Companies of all sizes are heavily dependent upon analytics to improve the user experience and forecast varying business data points. Gilt has leveraged Google Analytics (henceforth known as GA) heavily, utilizing the data redirection feature of GA. At some point in late 2015, that feature will be no more. Gumshoe was built to fill that void and to extend upon the data-collection abilities of GA.
10 |
11 | ## Browser Support
12 |
13 | Gumshoe supports the latest versions of:
14 |
15 | `Chrome`
16 |
17 | `Firefox`
18 |
19 | `Safari`
20 |
21 | And versions 8 - Latest of `Internet Explorer`. Sorry, but Gumshoe will not be supporting other `OldIE` versions.
22 |
23 |
24 | ## Structure
25 |
26 | Gumshoe is comprised of several simple sturctural elements:
27 |
28 | `dist` contains the compiled gumshoe files meant for distribution and use in the browser. Comes in minified and unminified flavors.
29 |
30 | `examples` contains small examples of working with Gumshoe.
31 |
32 | `lib` contains third party libraries bundled with Gumshoe which facilitate standardized and privately scoped functionality for each gumshoe instance.
33 |
34 | `src` contains the source for the Gumshoe library itself.
35 |
36 | `test` contains [mocha BDD](http://mochajs.org/) tests
37 |
38 | ## Project Goals
39 |
40 | - Parity with Google Analytics base page data
41 | - Organized event names and data
42 | - High level of data integrity and confidence
43 | - Low page footprint
44 | - Low failure and miss rate
45 |
46 | ## Base Concepts
47 |
48 | **Transport**
49 |
50 | Transports describe the way in which data is sent from Gumshoe to an endpoint where it is ultimately stored and/or analyized. Each implementation of Gumshoe is responsible for initializing its own transport. Once data for an event has been collected and the event has been queued, Gumshoe will attempt to send the data using the defined transport. Gumshoe also supports multiple transports for sending data to multiple endpoints.
51 |
52 | **Event Name**
53 |
54 | An event name should be carefully considered, and all event names should be of the same format, tense, and general structure. At Gilt we use a dot-delimited event naming notation. eg. `'checkout.country.selected'`.
55 |
56 | **Event Data**
57 |
58 | Event data can be anything, of any type, that you or your organization decide upon. At Gilt, all event data is serialized to a string using JSON.stringify. Event data should be chosen carefully, should be documented and should not change in structure for a particular event, if time-over-time reporting is a priority.
59 |
60 | ## Testing
61 |
62 | ```
63 | npm install
64 | gulp test
65 | ```
66 |
67 | To independently test the distribution version of Gumshoe, run:
68 |
69 | ```
70 | gulp test-dist
71 | ```
72 |
73 | ## Contributing
74 |
75 | Please fork the project and submit a pull request for all bugfixes, patches, or suggested improvements for Gumshoe.
76 |
77 | Please take into consideration our formatting style when submitting pull requests. Pull requests which don't follow our simple style guide won't be accepted.
78 |
79 | - Indentation is 2 spaces, no tabs.
80 | - `var` blocks should be separated by newlines.
81 | - Strings should be single-quoted
82 | - Logical and functional blocks should have a newline after the opening brace or paren, and before the closing brace or paren.
83 |
84 |
85 | ## Support
86 |
87 | Please post support requests and bugs to the Github Issues page for this project.
88 |
--------------------------------------------------------------------------------
/dist/gumshoe.js:
--------------------------------------------------------------------------------
1 | // polyfill for String.prototype.trim for IE8
2 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim
3 | if (!String.prototype.trim) {
4 | (function() {
5 | // Make sure we trim BOM and NBSP
6 | var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
7 | String.prototype.trim = function() {
8 | return this.replace(rtrim, '');
9 | };
10 | })();
11 | }
12 |
13 | // Production steps of ECMA-262, Edition 5, 15.4.4.21
14 | // Reference: http://es5.github.io/#x15.4.4.21
15 | if (!Array.prototype.reduce) {
16 | Array.prototype.reduce = function(callback /*, initialValue*/) {
17 | 'use strict';
18 | if (this == null) {
19 | throw new TypeError('Array.prototype.reduce called on null or undefined');
20 | }
21 | if (typeof callback !== 'function') {
22 | throw new TypeError(callback + ' is not a function');
23 | }
24 | var t = Object(this), len = t.length >>> 0, k = 0, value;
25 | if (arguments.length == 2) {
26 | value = arguments[1];
27 | } else {
28 | while (k < len && ! k in t) {
29 | k++;
30 | }
31 | if (k >= len) {
32 | throw new TypeError('Reduce of empty array with no initial value');
33 | }
34 | value = t[k++];
35 | }
36 | for (; k < len; k++) {
37 | if (k in t) {
38 | value = callback(value, t[k], k, t);
39 | }
40 | }
41 | return value;
42 | };
43 | }
44 | /**
45 | * @file perfnow is a 0.14 kb window.performance.now high resolution timer polyfill with Date fallback
46 | * @author Daniel Lamb
47 | */
48 | (function perfnow (window) {
49 | // make sure we have an object to work with
50 | if (!('performance' in window)) {
51 | window.performance = {};
52 | }
53 | var perf = window.performance;
54 | // handle vendor prefixing
55 | window.performance.now = perf.now ||
56 | perf.mozNow ||
57 | perf.msNow ||
58 | perf.oNow ||
59 | perf.webkitNow ||
60 | // fallback to Date
61 | Date.now || function () {
62 | return new Date().getTime();
63 | };
64 | })(window);
65 | /* global performance */
66 | (function (root) {
67 |
68 | 'use strict';
69 |
70 | // we need reqwest and store2 (and any other future deps)
71 | // to be solely within our context, so as they don't leak and conflict
72 | // with other versions of the same libs sites may be loading.
73 | // so we'll provide our own context.
74 | // root._gumshoe is only available in specs
75 | var context = root._gumshoe || {},
76 | queryString,
77 | store,
78 | /*jshint -W024 */
79 | undefined;
80 |
81 | // call contextSetup with 'context' as 'this' so all libs attach
82 | // to our context variable.
83 | (function contextSetup () {
84 | /*!
85 | query-string
86 | Parse and stringify URL query strings
87 | https://github.com/sindresorhus/query-string
88 | by Sindre Sorhus
89 | MIT License
90 | */
91 | (function (window) {
92 | 'use strict';
93 | var queryString = {};
94 |
95 | queryString.parse = function (str) {
96 | if (typeof str !== 'string') {
97 | return {};
98 | }
99 |
100 | str = str.trim().replace(/^(\?|#)/, '');
101 |
102 | if (!str) {
103 | return {};
104 | }
105 |
106 | return str.trim().split('&').reduce(function (ret, param) {
107 | var parts = param.replace(/\+/g, ' ').split('=');
108 | var key = parts[0];
109 | var val = parts[1];
110 |
111 | key = decodeURIComponent(key);
112 | // missing `=` should be `null`:
113 | // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
114 | val = val === undefined ? null : decodeURIComponent(val);
115 |
116 | if (!ret.hasOwnProperty(key)) {
117 | ret[key] = val;
118 | } else if (Array.isArray(ret[key])) {
119 | ret[key].push(val);
120 | } else {
121 | ret[key] = [ret[key], val];
122 | }
123 |
124 | return ret;
125 | }, {});
126 | };
127 |
128 | queryString.stringify = function (obj) {
129 | return obj ? Object.keys(obj).map(function (key) {
130 | var val = obj[key];
131 |
132 | if (Array.isArray(val)) {
133 | return val.map(function (val2) {
134 | return encodeURIComponent(key) + '=' + encodeURIComponent(val2);
135 | }).join('&');
136 | }
137 |
138 | return encodeURIComponent(key) + '=' + encodeURIComponent(val);
139 | }).join('&') : '';
140 | };
141 |
142 | if (typeof define === 'function' && define.amd) {
143 | define(function() { return queryString; });
144 | } else if (typeof module !== 'undefined' && module.exports) {
145 | module.exports = queryString;
146 | } else {
147 | window.queryString = queryString;
148 | }
149 | })(this);
150 |
151 |
152 | /*!
153 | * Reqwest! A general purpose XHR connection manager
154 | * license MIT (c) Dustin Diaz 2014
155 | * https://github.com/ded/reqwest
156 | */
157 |
158 | !function (name, context, definition) {
159 | if (typeof module != 'undefined' && module.exports) module.exports = definition()
160 | else if (typeof define == 'function' && define.amd) define(definition)
161 | else context[name] = definition()
162 | }('reqwest', this, function () {
163 |
164 | var win = window
165 | , doc = document
166 | , httpsRe = /^http/
167 | , protocolRe = /(^\w+):\/\//
168 | , twoHundo = /^(20\d|1223)$/ //http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request
169 | , byTag = 'getElementsByTagName'
170 | , readyState = 'readyState'
171 | , contentType = 'Content-Type'
172 | , requestedWith = 'X-Requested-With'
173 | , head = doc[byTag]('head')[0]
174 | , uniqid = 0
175 | , callbackPrefix = 'reqwest_' + (+new Date())
176 | , lastValue // data stored by the most recent JSONP callback
177 | , xmlHttpRequest = 'XMLHttpRequest'
178 | , xDomainRequest = 'XDomainRequest'
179 | , noop = function () {}
180 |
181 | , isArray = typeof Array.isArray == 'function'
182 | ? Array.isArray
183 | : function (a) {
184 | return a instanceof Array
185 | }
186 |
187 | , defaultHeaders = {
188 | 'contentType': 'application/x-www-form-urlencoded'
189 | , 'requestedWith': xmlHttpRequest
190 | , 'accept': {
191 | '*': 'text/javascript, text/html, application/xml, text/xml, */*'
192 | , 'xml': 'application/xml, text/xml'
193 | , 'html': 'text/html'
194 | , 'text': 'text/plain'
195 | , 'json': 'application/json, text/javascript'
196 | , 'js': 'application/javascript, text/javascript'
197 | }
198 | }
199 |
200 | , xhr = function(o) {
201 | // is it x-domain
202 | if (o['crossOrigin'] === true) {
203 | var xhr = win[xmlHttpRequest] ? new XMLHttpRequest() : null
204 | if (xhr && 'withCredentials' in xhr) {
205 | return xhr
206 | } else if (win[xDomainRequest]) {
207 | return new XDomainRequest()
208 | } else {
209 | throw new Error('Browser does not support cross-origin requests')
210 | }
211 | } else if (win[xmlHttpRequest]) {
212 | return new XMLHttpRequest()
213 | } else {
214 | return new ActiveXObject('Microsoft.XMLHTTP')
215 | }
216 | }
217 | , globalSetupOptions = {
218 | dataFilter: function (data) {
219 | return data
220 | }
221 | }
222 |
223 | function succeed(r) {
224 | var protocol = protocolRe.exec(r.url);
225 | protocol = (protocol && protocol[1]) || window.location.protocol;
226 | return httpsRe.test(protocol) ? twoHundo.test(r.request.status) : !!r.request.responseText;
227 | }
228 |
229 | function handleReadyState(r, success, error) {
230 | return function () {
231 | // use _aborted to mitigate against IE err c00c023f
232 | // (can't read props on aborted request objects)
233 | if (r._aborted) return error(r.request)
234 | if (r._timedOut) return error(r.request, 'Request is aborted: timeout')
235 | if (r.request && r.request[readyState] == 4) {
236 | r.request.onreadystatechange = noop
237 | if (succeed(r)) success(r.request)
238 | else
239 | error(r.request)
240 | }
241 | }
242 | }
243 |
244 | function setHeaders(http, o) {
245 | var headers = o['headers'] || {}
246 | , h
247 |
248 | headers['Accept'] = headers['Accept']
249 | || defaultHeaders['accept'][o['type']]
250 | || defaultHeaders['accept']['*']
251 |
252 | var isAFormData = typeof FormData === 'function' && (o['data'] instanceof FormData);
253 | // breaks cross-origin requests with legacy browsers
254 | if (!o['crossOrigin'] && !headers[requestedWith]) headers[requestedWith] = defaultHeaders['requestedWith']
255 | if (!headers[contentType] && !isAFormData) headers[contentType] = o['contentType'] || defaultHeaders['contentType']
256 | for (h in headers)
257 | headers.hasOwnProperty(h) && 'setRequestHeader' in http && http.setRequestHeader(h, headers[h])
258 | }
259 |
260 | function setCredentials(http, o) {
261 | if (typeof o['withCredentials'] !== 'undefined' && typeof http.withCredentials !== 'undefined') {
262 | http.withCredentials = !!o['withCredentials']
263 | }
264 | }
265 |
266 | function generalCallback(data) {
267 | lastValue = data
268 | }
269 |
270 | function urlappend (url, s) {
271 | return url + (/\?/.test(url) ? '&' : '?') + s
272 | }
273 |
274 | function handleJsonp(o, fn, err, url) {
275 | var reqId = uniqid++
276 | , cbkey = o['jsonpCallback'] || 'callback' // the 'callback' key
277 | , cbval = o['jsonpCallbackName'] || reqwest.getcallbackPrefix(reqId)
278 | , cbreg = new RegExp('((^|\\?|&)' + cbkey + ')=([^&]+)')
279 | , match = url.match(cbreg)
280 | , script = doc.createElement('script')
281 | , loaded = 0
282 | , isIE10 = navigator.userAgent.indexOf('MSIE 10.0') !== -1
283 |
284 | if (match) {
285 | if (match[3] === '?') {
286 | url = url.replace(cbreg, '$1=' + cbval) // wildcard callback func name
287 | } else {
288 | cbval = match[3] // provided callback func name
289 | }
290 | } else {
291 | url = urlappend(url, cbkey + '=' + cbval) // no callback details, add 'em
292 | }
293 |
294 | win[cbval] = generalCallback
295 |
296 | script.type = 'text/javascript'
297 | script.src = url
298 | script.async = true
299 | if (typeof script.onreadystatechange !== 'undefined' && !isIE10) {
300 | // need this for IE due to out-of-order onreadystatechange(), binding script
301 | // execution to an event listener gives us control over when the script
302 | // is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
303 | script.htmlFor = script.id = '_reqwest_' + reqId
304 | }
305 |
306 | script.onload = script.onreadystatechange = function () {
307 | if ((script[readyState] && script[readyState] !== 'complete' && script[readyState] !== 'loaded') || loaded) {
308 | return false
309 | }
310 | script.onload = script.onreadystatechange = null
311 | script.onclick && script.onclick()
312 | // Call the user callback with the last value stored and clean up values and scripts.
313 | fn(lastValue)
314 | lastValue = undefined
315 | head.removeChild(script)
316 | loaded = 1
317 | }
318 |
319 | // Add the script to the DOM head
320 | head.appendChild(script)
321 |
322 | // Enable JSONP timeout
323 | return {
324 | abort: function () {
325 | script.onload = script.onreadystatechange = null
326 | err({}, 'Request is aborted: timeout', {})
327 | lastValue = undefined
328 | head.removeChild(script)
329 | loaded = 1
330 | }
331 | }
332 | }
333 |
334 | function getRequest(fn, err) {
335 | var o = this.o
336 | , method = (o['method'] || 'GET').toUpperCase()
337 | , url = typeof o === 'string' ? o : o['url']
338 | // convert non-string objects to query-string form unless o['processData'] is false
339 | , data = (o['processData'] !== false && o['data'] && typeof o['data'] !== 'string')
340 | ? reqwest.toQueryString(o['data'])
341 | : (o['data'] || null)
342 | , http
343 | , sendWait = false
344 |
345 | // if we're working on a GET request and we have data then we should append
346 | // query string to end of URL and not post data
347 | if ((o['type'] == 'jsonp' || method == 'GET') && data) {
348 | url = urlappend(url, data)
349 | data = null
350 | }
351 |
352 | if (o['type'] == 'jsonp') return handleJsonp(o, fn, err, url)
353 |
354 | // get the xhr from the factory if passed
355 | // if the factory returns null, fall-back to ours
356 | http = (o.xhr && o.xhr(o)) || xhr(o)
357 |
358 | http.open(method, url, o['async'] === false ? false : true)
359 | setHeaders(http, o)
360 | setCredentials(http, o)
361 | if (win[xDomainRequest] && http instanceof win[xDomainRequest]) {
362 | http.onload = fn
363 | http.onerror = err
364 | // NOTE: see
365 | // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/30ef3add-767c-4436-b8a9-f1ca19b4812e
366 | http.onprogress = function() {}
367 | sendWait = true
368 | } else {
369 | http.onreadystatechange = handleReadyState(this, fn, err)
370 | }
371 | o['before'] && o['before'](http)
372 | if (sendWait) {
373 | setTimeout(function () {
374 | http.send(data)
375 | }, 200)
376 | } else {
377 | http.send(data)
378 | }
379 | return http
380 | }
381 |
382 | function Reqwest(o, fn) {
383 | this.o = o
384 | this.fn = fn
385 |
386 | init.apply(this, arguments)
387 | }
388 |
389 | function setType(header) {
390 | // json, javascript, text/plain, text/html, xml
391 | if (header.match('json')) return 'json'
392 | if (header.match('javascript')) return 'js'
393 | if (header.match('text')) return 'html'
394 | if (header.match('xml')) return 'xml'
395 | }
396 |
397 | function init(o, fn) {
398 |
399 | this.url = typeof o == 'string' ? o : o['url']
400 | this.timeout = null
401 |
402 | // whether request has been fulfilled for purpose
403 | // of tracking the Promises
404 | this._fulfilled = false
405 | // success handlers
406 | this._successHandler = function(){}
407 | this._fulfillmentHandlers = []
408 | // error handlers
409 | this._errorHandlers = []
410 | // complete (both success and fail) handlers
411 | this._completeHandlers = []
412 | this._erred = false
413 | this._responseArgs = {}
414 |
415 | var self = this
416 |
417 | fn = fn || function () {}
418 |
419 | if (o['timeout']) {
420 | this.timeout = setTimeout(function () {
421 | timedOut()
422 | }, o['timeout'])
423 | }
424 |
425 | if (o['success']) {
426 | this._successHandler = function () {
427 | o['success'].apply(o, arguments)
428 | }
429 | }
430 |
431 | if (o['error']) {
432 | this._errorHandlers.push(function () {
433 | o['error'].apply(o, arguments)
434 | })
435 | }
436 |
437 | if (o['complete']) {
438 | this._completeHandlers.push(function () {
439 | o['complete'].apply(o, arguments)
440 | })
441 | }
442 |
443 | function complete (resp) {
444 | o['timeout'] && clearTimeout(self.timeout)
445 | self.timeout = null
446 | while (self._completeHandlers.length > 0) {
447 | self._completeHandlers.shift()(resp)
448 | }
449 | }
450 |
451 | function success (resp) {
452 | var type = o['type'] || resp && setType(resp.getResponseHeader('Content-Type')) // resp can be undefined in IE
453 | resp = (type !== 'jsonp') ? self.request : resp
454 | // use global data filter on response text
455 | var filteredResponse = globalSetupOptions.dataFilter(resp.responseText, type)
456 | , r = filteredResponse
457 | try {
458 | resp.responseText = r
459 | } catch (e) {
460 | // can't assign this in IE<=8, just ignore
461 | }
462 | if (r) {
463 | switch (type) {
464 | case 'json':
465 | try {
466 | resp = win.JSON ? win.JSON.parse(r) : eval('(' + r + ')')
467 | } catch (err) {
468 | return error(resp, 'Could not parse JSON in response', err)
469 | }
470 | break
471 | case 'js':
472 | resp = eval(r)
473 | break
474 | case 'html':
475 | resp = r
476 | break
477 | case 'xml':
478 | resp = resp.responseXML
479 | && resp.responseXML.parseError // IE trololo
480 | && resp.responseXML.parseError.errorCode
481 | && resp.responseXML.parseError.reason
482 | ? null
483 | : resp.responseXML
484 | break
485 | }
486 | }
487 |
488 | self._responseArgs.resp = resp
489 | self._fulfilled = true
490 | fn(resp)
491 | self._successHandler(resp)
492 | while (self._fulfillmentHandlers.length > 0) {
493 | resp = self._fulfillmentHandlers.shift()(resp)
494 | }
495 |
496 | complete(resp)
497 | }
498 |
499 | function timedOut() {
500 | self._timedOut = true
501 | if(typeof self.request !== 'undefined' && typeof self.request.abort === 'function') {
502 | self.request.abort();
503 | }
504 | }
505 |
506 | function error(resp, msg, t) {
507 | resp = self.request
508 | self._responseArgs.resp = resp
509 | self._responseArgs.msg = msg
510 | self._responseArgs.t = t
511 | self._erred = true
512 | while (self._errorHandlers.length > 0) {
513 | self._errorHandlers.shift()(resp, msg, t)
514 | }
515 | complete(resp)
516 | }
517 |
518 | this.request = getRequest.call(this, success, error)
519 | }
520 |
521 | Reqwest.prototype = {
522 | abort: function () {
523 | this._aborted = true
524 | if(typeof this.request !== 'undefined' && typeof this.request.abort === 'function') {
525 | this.request.abort();
526 | }
527 | }
528 |
529 | , retry: function () {
530 | this._aborted=false;
531 | this._timedOut=false;
532 | init.call(this, this.o, this.fn)
533 | }
534 |
535 | /**
536 | * Small deviation from the Promises A CommonJs specification
537 | * http://wiki.commonjs.org/wiki/Promises/A
538 | */
539 |
540 | /**
541 | * `then` will execute upon successful requests
542 | */
543 | , then: function (success, fail) {
544 | success = success || function () {}
545 | fail = fail || function () {}
546 | if (this._fulfilled) {
547 | this._responseArgs.resp = success(this._responseArgs.resp)
548 | } else if (this._erred) {
549 | fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
550 | } else {
551 | this._fulfillmentHandlers.push(success)
552 | this._errorHandlers.push(fail)
553 | }
554 | return this
555 | }
556 |
557 | /**
558 | * `always` will execute whether the request succeeds or fails
559 | */
560 | , always: function (fn) {
561 | if (this._fulfilled || this._erred) {
562 | fn(this._responseArgs.resp)
563 | } else {
564 | this._completeHandlers.push(fn)
565 | }
566 | return this
567 | }
568 |
569 | /**
570 | * `fail` will execute when the request fails
571 | */
572 | , fail: function (fn) {
573 | if (this._erred) {
574 | fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
575 | } else {
576 | this._errorHandlers.push(fn)
577 | }
578 | return this
579 | }
580 | , 'catch': function (fn) {
581 | return this.fail(fn)
582 | }
583 | }
584 |
585 | function reqwest(o, fn) {
586 | return new Reqwest(o, fn)
587 | }
588 |
589 | // normalize newline variants according to spec -> CRLF
590 | function normalize(s) {
591 | return s ? s.replace(/\r?\n/g, '\r\n') : ''
592 | }
593 |
594 | function serial(el, cb) {
595 | var n = el.name
596 | , t = el.tagName.toLowerCase()
597 | , optCb = function (o) {
598 | // IE gives value="" even where there is no value attribute
599 | // 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273
600 | if (o && !o['disabled'])
601 | cb(n, normalize(o['attributes']['value'] && o['attributes']['value']['specified'] ? o['value'] : o['text']))
602 | }
603 | , ch, ra, val, i
604 |
605 | // don't serialize elements that are disabled or without a name
606 | if (el.disabled || !n) return
607 |
608 | switch (t) {
609 | case 'input':
610 | if (!/reset|button|image|file/i.test(el.type)) {
611 | ch = /checkbox/i.test(el.type)
612 | ra = /radio/i.test(el.type)
613 | val = el.value
614 | // WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here
615 | ;(!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val))
616 | }
617 | break
618 | case 'textarea':
619 | cb(n, normalize(el.value))
620 | break
621 | case 'select':
622 | if (el.type.toLowerCase() === 'select-one') {
623 | optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null)
624 | } else {
625 | for (i = 0; el.length && i < el.length; i++) {
626 | el.options[i].selected && optCb(el.options[i])
627 | }
628 | }
629 | break
630 | }
631 | }
632 |
633 | // collect up all form elements found from the passed argument elements all
634 | // the way down to child elements; pass a '