[#172,refactor][s]: switch everything to use underscore.deferred rather than jQuery.Deferred - fixes #172.

* In addition reduced pattern of passing in $ to backend modules - instead just use jQuery explicitly (this should make it easier to mock-out jQuery if you waned to
This commit is contained in:
Rufus Pollock 2013-01-04 20:13:16 +00:00
parent e09a793a85
commit 965bf6e9bb
14 changed files with 505 additions and 48 deletions

View File

@ -57,6 +57,7 @@ Possible breaking changes
* Dataset.restore method removed (not used internally except from Multiview.restore)
* Views no longer call render in initialize but must be called client code
* Backend.Memory.Store attribute for holding 'records' renamed to `records` from `data`
* Require new underscore.deferred vendor library for all use (jQuery no longer required if just using recline.dataset.js)
### v0.5 - July 5th 2012 (first public release)

View File

@ -22,6 +22,7 @@
<!-- 3rd party JS libraries -->
<script type="text/javascript" src="{{page.root}}vendor/jquery/1.7.1/jquery.js"></script>
<script type="text/javascript" src="{{page.root}}vendor/underscore/1.4.2/underscore.js"></script>
<script type="text/javascript" src="{{page.root}}vendor/underscore.deferred/0.4.0/underscore.deferred.js"></script>
<script type="text/javascript" src="{{page.root}}vendor/backbone/0.9.2/backbone.js"></script>
<script type="text/javascript" src="{{page.root}}vendor/mustache/0.5.0-dev/mustache.js"></script>
<script type="text/javascript" src="{{page.root}}vendor/bootstrap/2.0.2/bootstrap.js"></script>

View File

@ -68,15 +68,23 @@ title: Download
### Dependencies
Recline has dependencies on some third-party libraries, notably JQuery and Backbone:
Recline has dependencies on some third-party libraries. Specifically, recline.dataset.js depends on:
* [Underscore](http://documentcloud.github.com/underscore/) &gt;= 1.0
* [Underscore Deferred](https://github.com/wookiehangover/underscore.deferred) v0.4.0
* [Backbone](http://backbonejs.org/) >= 0.5.1
Those backends which utilize jquery's ajax method depend on jQuery:
* [JQuery](http://jquery.com/) >= 1.6
* [Backbone](http://backbonejs.org/) >= 0.5.1
* [Underscore](http://documentcloud.github.com/underscore/) &gt;= 1.0
Optional dependencies:
All the views require, in addition to those needed for recline.dataset.js:
* [JQuery](http://jquery.com/) >= 1.6
* [Mustache.js](https://github.com/janl/mustache.js/) &gt;= 0.5.0-dev (required for all views)
Individual views have additional dependencies such as:
* [JQuery Flot](http://code.google.com/p/flot/) >= 0.7 (required for for graph view)
* [Leaflet](http://leaflet.cloudmade.com/) >= 0.4.4 (required for map view)
* [Leaflet.markercluster](https://github.com/danzel/Leaflet.markercluster) as of 2012-09-12 (required for marker clustering)
@ -84,7 +92,8 @@ Optional dependencies:
* [Bootstrap](http://twitter.github.com/bootstrap/) &gt;= v2.0 (default option for CSS and UI JS but you can use your own)
If you grab the full zipball for Recline this will include all of the relevant
dependencies in the vendor directory.
dependencies in the vendor directory and you can also find them at in the
[github repo here](https://github.com/okfn/recline/tree/master/vendor).
### Example

View File

@ -2,7 +2,7 @@ this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
(function($, my) {
(function(my) {
// ## CKAN Backend
//
// This provides connection to the CKAN DataStore (v2)
@ -41,7 +41,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
dataset.id = out.resource_id;
var wrapper = my.DataStore(out.endpoint);
}
var dfd = $.Deferred();
var dfd = new _.Deferred();
var jqxhr = wrapper.search({resource_id: dataset.id, limit: 0});
jqxhr.done(function(results) {
// map ckan types to our usual types ...
@ -84,7 +84,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
var wrapper = my.DataStore(out.endpoint);
}
var actualQuery = my._normalizeQuery(queryObj, dataset);
var dfd = $.Deferred();
var dfd = new _.Deferred();
var jqxhr = wrapper.search(actualQuery);
jqxhr.done(function(results) {
var out = {
@ -107,7 +107,7 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
};
that.search = function(data) {
var searchUrl = that.endpoint + '/3/action/datastore_search';
var jqxhr = $.ajax({
var jqxhr = jQuery.ajax({
url: searchUrl,
data: data,
dataType: 'json'
@ -136,4 +136,4 @@ this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
'float8': 'float'
};
}(jQuery, this.recline.Backend.Ckan));
}(this.recline.Backend.Ckan));

View File

@ -197,7 +197,7 @@ my.__type__ = 'couchdb';
var db_url = dataset.db_url;
var view_url = dataset.view_url;
var cdb = new my.CouchDBWrapper(db_url, view_url);
var dfd = $.Deferred();
var dfd = new _.Deferred();
// if 'doc' attribute is present, return schema of that
// else return schema of 'value' attribute which contains
@ -239,7 +239,7 @@ my.__type__ = 'couchdb';
//
//
my.save = function (changes, dataset) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var total = changes.creates.length + changes.updates.length + changes.deletes.length;
var results = {'done': [], 'fail': [] };
@ -280,7 +280,7 @@ my.save = function (changes, dataset) {
// @param {Object} recline.Dataset instance
// @param {Object} recline.Query instance.
my.query = function(queryObj, dataset) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var db_url = dataset.db_url;
var view_url = dataset.view_url;
var query_options = dataset.query_options;
@ -475,7 +475,7 @@ function randomId(length, chars) {
}
_createDocument = function (new_doc, dataset) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var db_url = dataset.db_url;
var view_url = dataset.view_url;
var _id = new_doc['id'];
@ -497,7 +497,7 @@ _createDocument = function (new_doc, dataset) {
};
_updateDocument = function (new_doc, dataset) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var db_url = dataset.db_url;
var view_url = dataset.view_url;
var _id = new_doc['id'];
@ -527,7 +527,7 @@ _updateDocument = function (new_doc, dataset) {
};
_deleteDocument = function (del_doc, dataset) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var db_url = dataset.db_url;
var view_url = dataset.view_url;
var _id = del_doc['id'];
@ -557,4 +557,4 @@ _deleteDocument = function (del_doc, dataset) {
return dfd.promise();
}
};
}(jQuery, this.recline.Backend.CouchDB));
}(jQuery, this.recline.Backend.CouchDB));

View File

@ -3,7 +3,7 @@ this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.CSV = this.recline.Backend.CSV || {};
// Note that provision of jQuery is optional (it is **only** needed if you use fetch on a remote file)
(function(my, $) {
(function(my) {
// ## fetch
//
@ -11,7 +11,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
//
// 1. `dataset.file`: `file` is an HTML5 file object. This is opened and parsed with the CSV parser.
// 2. `dataset.data`: `data` is a string in CSV format. This is passed directly to the CSV parser
// 3. `dataset.url`: a url to an online CSV file that is ajax accessible (note this usually requires either local or on a server that is CORS enabled). The file is then loaded using $.ajax and parsed using the CSV parser (NB: this requires jQuery)
// 3. `dataset.url`: a url to an online CSV file that is ajax accessible (note this usually requires either local or on a server that is CORS enabled). The file is then loaded using jQuery.ajax and parsed using the CSV parser (NB: this requires jQuery)
//
// All options generates similar data and use the memory store outcome, that is they return something like:
//
@ -23,7 +23,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
// }
// </pre>
my.fetch = function(dataset) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
if (dataset.file) {
var reader = new FileReader();
var encoding = dataset.encoding || 'UTF-8';
@ -48,7 +48,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
useMemoryStore: true
});
} else if (dataset.url) {
$.get(dataset.url).done(function(data) {
jQuery.get(dataset.url).done(function(data) {
var rows = my.parseCSV(data, dataset);
dfd.resolve({
records: rows,
@ -285,4 +285,4 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
}
}(this.recline.Backend.CSV, jQuery));
}(this.recline.Backend.CSV));

View File

@ -2,7 +2,7 @@ this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
(function($, my) {
(function(my) {
my.__type__ = 'dataproxy';
// URL for the dataproxy
my.dataproxy_url = 'http://jsonpdataproxy.appspot.com';
@ -21,12 +21,12 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
'max-results': dataset.size || dataset.rows || 1000,
type: dataset.format || ''
};
var jqxhr = $.ajax({
var jqxhr = jQuery.ajax({
url: my.dataproxy_url,
data: data,
dataType: 'jsonp'
});
var dfd = $.Deferred();
var dfd = new _.Deferred();
_wrapInTimeout(jqxhr).done(function(results) {
if (results.error) {
dfd.reject(results.error);
@ -50,7 +50,7 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
// Many of backends use JSONP and so will not get error messages and this is
// a crude way to catch those errors.
var _wrapInTimeout = function(ourFunction) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var timer = setTimeout(function() {
dfd.reject({
message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
@ -68,4 +68,4 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
return dfd.promise();
}
}(jQuery, this.recline.Backend.DataProxy));
}(this.recline.Backend.DataProxy));

View File

@ -179,7 +179,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
// ### fetch
my.fetch = function(dataset) {
var es = new my.Wrapper(dataset.url, my.esOptions);
var dfd = $.Deferred();
var dfd = new _.Deferred();
es.mapping().done(function(schema) {
if (!schema){
@ -207,7 +207,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
my.save = function(changes, dataset) {
var es = new my.Wrapper(dataset.url, my.esOptions);
if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
msg = 'Saving more than one item at a time not yet supported';
alert(msg);
dfd.reject(msg);
@ -225,7 +225,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
// ### query
my.query = function(queryObj, dataset) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var es = new my.Wrapper(dataset.url, my.esOptions);
var jqxhr = es.query(queryObj);
jqxhr.done(function(results) {

View File

@ -2,7 +2,7 @@ this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
(function($, my) {
(function(my) {
my.__type__ = 'gdocs';
// ## Google spreadsheet backend
@ -29,15 +29,15 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
// * fields: array of Field objects
// * records: array of objects for each row
my.fetch = function(dataset) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var urls = my.getGDocsAPIUrls(dataset.url);
// TODO cover it with tests
// get the spreadsheet title
(function () {
var titleDfd = $.Deferred();
var titleDfd = new _.Deferred();
$.getJSON(urls.spreadsheet, function (d) {
jQuery.getJSON(urls.spreadsheet, function (d) {
titleDfd.resolve({
spreadsheetTitle: d.feed.title.$t
});
@ -47,7 +47,7 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
}()).then(function (response) {
// get the actual worksheet data
$.getJSON(urls.worksheet, function(d) {
jQuery.getJSON(urls.worksheet, function(d) {
var result = my.parseData(d);
var fields = _.map(result.fields, function(fieldId) {
return {id: fieldId};
@ -161,4 +161,4 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
return urls;
};
}(jQuery, this.recline.Backend.GDocs));
}(this.recline.Backend.GDocs));

View File

@ -2,7 +2,7 @@ this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.Memory = this.recline.Backend.Memory || {};
(function($, my) {
(function(my) {
my.__type__ = 'memory';
// ## Data Wrapper
@ -48,7 +48,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
this.save = function(changes, dataset) {
var self = this;
var dfd = $.Deferred();
var dfd = new _.Deferred();
// TODO _.each(changes.creates) { ... }
_.each(changes.updates, function(record) {
self.update(record);
@ -61,7 +61,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
},
this.query = function(queryObj) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
var numRows = queryObj.size || this.records.length;
var start = queryObj.from || 0;
var results = this.records;
@ -229,7 +229,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
};
this.transform = function(editFunc) {
var dfd = $.Deferred();
var dfd = new _.Deferred();
// TODO: should we clone before mapping? Do not see the point atm.
self.records = _.map(self.records, editFunc);
// now deal with deletes (i.e. nulls)
@ -241,4 +241,4 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
};
};
}(jQuery, this.recline.Backend.Memory));
}(this.recline.Backend.Memory));

View File

@ -18,7 +18,7 @@ this.recline.Backend.Solr = this.recline.Backend.Solr || {};
dataType: 'jsonp',
jsonp: 'json.wrf'
});
var dfd = $.Deferred();
var dfd = new _.Deferred();
jqxhr.done(function(results) {
// if we get 0 results we cannot get fields
var fields = []
@ -51,7 +51,7 @@ this.recline.Backend.Solr = this.recline.Backend.Solr || {};
dataType: 'jsonp',
jsonp: 'json.wrf'
});
var dfd = $.Deferred();
var dfd = new _.Deferred();
jqxhr.done(function(results) {
var out = {
total: results.response.numFound,

View File

@ -2,7 +2,7 @@
this.recline = this.recline || {};
this.recline.Model = this.recline.Model || {};
(function($, my) {
(function(my) {
// ## <a id="dataset">Dataset</a>
my.Dataset = Backbone.Model.extend({
@ -47,7 +47,7 @@ my.Dataset = Backbone.Model.extend({
// Retrieve dataset and (some) records from the backend.
fetch: function() {
var self = this;
var dfd = $.Deferred();
var dfd = new _.Deferred();
if (this.backend !== recline.Backend.Memory) {
this.backend.fetch(this.toJSON())
@ -181,7 +181,7 @@ my.Dataset = Backbone.Model.extend({
// also returned.
query: function(queryObj) {
var self = this;
var dfd = $.Deferred();
var dfd = new _.Deferred();
this.trigger('query:start');
if (queryObj) {
@ -245,7 +245,7 @@ my.Dataset = Backbone.Model.extend({
this.fields.each(function(field) {
query.addFacet(field.id);
});
var dfd = $.Deferred();
var dfd = new _.Deferred();
this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
if (queryResult.facets) {
_.each(queryResult.facets, function(facetResult, facetId) {
@ -585,5 +585,5 @@ Backbone.sync = function(method, model, options) {
return model.backend.sync(method, model, options);
};
}(jQuery, this.recline.Model));
}(this.recline.Model));

View File

@ -10,6 +10,7 @@
<script type="text/javascript" src="../vendor/jquery/1.7.1/jquery.js"></script>
<script type="text/javascript" src="../vendor/underscore/1.4.2/underscore.js"></script>
<script type="text/javascript" src="../vendor/underscore.deferred/0.4.0/underscore.deferred.js"></script>
<script type="text/javascript" src="../vendor/backbone/0.9.2/backbone.js"></script>
<script type="text/javascript" src="../vendor/moment/1.6.2/moment.js"></script>
<script type="text/javascript" src="../vendor/mustache/0.5.0-dev/mustache.js"></script>

View File

@ -0,0 +1,445 @@
(function(root){
// Let's borrow a couple of things from Underscore that we'll need
// _.each
var breaker = {},
AP = Array.prototype,
OP = Object.prototype,
hasOwn = OP.hasOwnProperty,
toString = OP.toString,
forEach = AP.forEach,
indexOf = AP.indexOf,
slice = AP.slice;
var _each = function( obj, iterator, context ) {
var key, i, l;
if ( !obj ) {
return;
}
if ( forEach && obj.forEach === forEach ) {
obj.forEach( iterator, context );
} else if ( obj.length === +obj.length ) {
for ( i = 0, l = obj.length; i < l; i++ ) {
if ( i in obj && iterator.call( context, obj[i], i, obj ) === breaker ) {
return;
}
}
} else {
for ( key in obj ) {
if ( hasOwn.call( obj, key ) ) {
if ( iterator.call( context, obj[key], key, obj) === breaker ) {
return;
}
}
}
}
};
// _.isFunction
var _isFunction = function( obj ) {
return !!(obj && obj.constructor && obj.call && obj.apply);
};
// _.extend
var _extend = function( obj ) {
_each( slice.call( arguments, 1), function( source ) {
var prop;
for ( prop in source ) {
if ( source[prop] !== void 0 ) {
obj[ prop ] = source[ prop ];
}
}
});
return obj;
};
// $.inArray
var _inArray = function( elem, arr, i ) {
var len;
if ( arr ) {
if ( indexOf ) {
return indexOf.call( arr, elem, i );
}
len = arr.length;
i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
for ( ; i < len; i++ ) {
// Skip accessing in sparse arrays
if ( i in arr && arr[ i ] === elem ) {
return i;
}
}
}
return -1;
};
// And some jQuery specific helpers
var class2type = {};
// Populate the class2type map
_each("Boolean Number String Function Array Date RegExp Object".split(" "), function(name, i) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
});
var _type = function( obj ) {
return obj == null ?
String( obj ) :
class2type[ toString.call(obj) ] || "object";
};
// Now start the jQuery-cum-Underscore implementation. Some very
// minor changes to the jQuery source to get this working.
// Internal Deferred namespace
var _d = {};
// String to Object options format cache
var optionsCache = {};
// Convert String-formatted options into Object-formatted ones and store in cache
function createOptions( options ) {
var object = optionsCache[ options ] = {};
_each( options.split( /\s+/ ), function( flag ) {
object[ flag ] = true;
});
return object;
}
_d.Callbacks = function( options ) {
// Convert options from String-formatted to Object-formatted if needed
// (we check in cache first)
options = typeof options === "string" ?
( optionsCache[ options ] || createOptions( options ) ) :
_extend( {}, options );
var // Last fire value (for non-forgettable lists)
memory,
// Flag to know if list was already fired
fired,
// Flag to know if list is currently firing
firing,
// First callback to fire (used internally by add and fireWith)
firingStart,
// End of the loop when firing
firingLength,
// Index of currently firing callback (modified by remove if needed)
firingIndex,
// Actual callback list
list = [],
// Stack of fire calls for repeatable lists
stack = !options.once && [],
// Fire callbacks
fire = function( data ) {
memory = options.memory && data;
fired = true;
firingIndex = firingStart || 0;
firingStart = 0;
firingLength = list.length;
firing = true;
for ( ; list && firingIndex < firingLength; firingIndex++ ) {
if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
memory = false; // To prevent further calls using add
break;
}
}
firing = false;
if ( list ) {
if ( stack ) {
if ( stack.length ) {
fire( stack.shift() );
}
} else if ( memory ) {
list = [];
} else {
self.disable();
}
}
},
// Actual Callbacks object
self = {
// Add a callback or a collection of callbacks to the list
add: function() {
if ( list ) {
// First, we save the current length
var start = list.length;
(function add( args ) {
_each( args, function( arg ) {
var type = _type( arg );
if ( type === "function" ) {
if ( !options.unique || !self.has( arg ) ) {
list.push( arg );
}
} else if ( arg && arg.length && type !== "string" ) {
// Inspect recursively
add( arg );
}
});
})( arguments );
// Do we need to add the callbacks to the
// current firing batch?
if ( firing ) {
firingLength = list.length;
// With memory, if we're not firing then
// we should call right away
} else if ( memory ) {
firingStart = start;
fire( memory );
}
}
return this;
},
// Remove a callback from the list
remove: function() {
if ( list ) {
_each( arguments, function( arg ) {
var index;
while( ( index = _inArray( arg, list, index ) ) > -1 ) {
list.splice( index, 1 );
// Handle firing indexes
if ( firing ) {
if ( index <= firingLength ) {
firingLength--;
}
if ( index <= firingIndex ) {
firingIndex--;
}
}
}
});
}
return this;
},
// Control if a given callback is in the list
has: function( fn ) {
return _inArray( fn, list ) > -1;
},
// Remove all callbacks from the list
empty: function() {
list = [];
return this;
},
// Have the list do nothing anymore
disable: function() {
list = stack = memory = undefined;
return this;
},
// Is it disabled?
disabled: function() {
return !list;
},
// Lock the list in its current state
lock: function() {
stack = undefined;
if ( !memory ) {
self.disable();
}
return this;
},
// Is it locked?
locked: function() {
return !stack;
},
// Call all callbacks with the given context and arguments
fireWith: function( context, args ) {
args = args || [];
args = [ context, args.slice ? args.slice() : args ];
if ( list && ( !fired || stack ) ) {
if ( firing ) {
stack.push( args );
} else {
fire( args );
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith( this, arguments );
return this;
},
// To know if the callbacks have already been called at least once
fired: function() {
return !!fired;
}
};
return self;
};
_d.Deferred = function( func ) {
var tuples = [
// action, add listener, listener list, final state
[ "resolve", "done", _d.Callbacks("once memory"), "resolved" ],
[ "reject", "fail", _d.Callbacks("once memory"), "rejected" ],
[ "notify", "progress", _d.Callbacks("memory") ]
],
state = "pending",
promise = {
state: function() {
return state;
},
always: function() {
deferred.done( arguments ).fail( arguments );
return this;
},
then: function( /* fnDone, fnFail, fnProgress */ ) {
var fns = arguments;
return _d.Deferred(function( newDefer ) {
_each( tuples, function( tuple, i ) {
var action = tuple[ 0 ],
fn = fns[ i ];
// deferred[ done | fail | progress ] for forwarding actions to newDefer
deferred[ tuple[1] ]( _isFunction( fn ) ?
function() {
var returned;
try { returned = fn.apply( this, arguments ); } catch(e){
newDefer.reject(e);
return;
}
if ( returned && _isFunction( returned.promise ) ) {
returned.promise()
.done( newDefer.resolve )
.fail( newDefer.reject )
.progress( newDefer.notify );
} else {
newDefer[ action !== "notify" ? 'resolveWith' : action + 'With']( this === deferred ? newDefer : this, [ returned ] );
}
} :
newDefer[ action ]
);
});
fns = null;
}).promise();
},
// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
promise: function( obj ) {
return obj != null ? _extend( obj, promise ) : promise;
}
},
deferred = {};
// Keep pipe for back-compat
promise.pipe = promise.then;
// Add list-specific methods
_each( tuples, function( tuple, i ) {
var list = tuple[ 2 ],
stateString = tuple[ 3 ];
// promise[ done | fail | progress ] = list.add
promise[ tuple[1] ] = list.add;
// Handle state
if ( stateString ) {
list.add(function() {
// state = [ resolved | rejected ]
state = stateString;
// [ reject_list | resolve_list ].disable; progress_list.lock
}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
}
// deferred[ resolve | reject | notify ] = list.fire
deferred[ tuple[0] ] = list.fire;
deferred[ tuple[0] + "With" ] = list.fireWith;
});
// Make the deferred a promise
promise.promise( deferred );
// Call given func if any
if ( func ) {
func.call( deferred, deferred );
}
// All done!
return deferred;
};
// Deferred helper
_d.when = function( subordinate /* , ..., subordinateN */ ) {
var i = 0,
resolveValues = _type(subordinate) === 'array' && arguments.length === 1 ?
subordinate : slice.call( arguments ),
length = resolveValues.length,
// the count of uncompleted subordinates
remaining = length !== 1 || ( subordinate && _isFunction( subordinate.promise ) ) ? length : 0,
// the master Deferred. If resolveValues consist of only a single Deferred, just use that.
deferred = remaining === 1 ? subordinate : _d.Deferred(),
// Update function for both resolve and progress values
updateFunc = function( i, contexts, values ) {
return function( value ) {
contexts[ i ] = this;
values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
if( values === progressValues ) {
deferred.notifyWith( contexts, values );
} else if ( !( --remaining ) ) {
deferred.resolveWith( contexts, values );
}
};
},
progressValues, progressContexts, resolveContexts;
// add listeners to Deferred subordinates; treat others as resolved
if ( length > 1 ) {
progressValues = new Array( length );
progressContexts = new Array( length );
resolveContexts = new Array( length );
for ( ; i < length; i++ ) {
if ( resolveValues[ i ] && _isFunction( resolveValues[ i ].promise ) ) {
resolveValues[ i ].promise()
.done( updateFunc( i, resolveContexts, resolveValues ) )
.fail( deferred.reject )
.progress( updateFunc( i, progressContexts, progressValues ) );
} else {
--remaining;
}
}
}
// if we're not waiting on anything, resolve the master
if ( !remaining ) {
deferred.resolveWith( resolveContexts, resolveValues );
}
return deferred.promise();
};
// Try exporting as a Common.js Module
if ( typeof module !== "undefined" && module.exports ) {
module.exports = _d;
// Or mixin to Underscore.js
} else if ( typeof root._ !== "undefined" ) {
root._.mixin(_d);
// Or assign it to window._
} else {
root._ = _d;
}
})(this);