From 965bf6e9bbe7df80bf8b0f25094a13adee790bde Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 4 Jan 2013 20:13:16 +0000 Subject: [PATCH] [#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 --- README.md | 1 + _includes/recline-deps.html | 1 + download.markdown | 19 +- src/backend.ckan.js | 10 +- src/backend.couchdb.js | 14 +- src/backend.csv.js | 10 +- src/backend.dataproxy.js | 10 +- src/backend.elasticsearch.js | 6 +- src/backend.gdocs.js | 12 +- src/backend.memory.js | 10 +- src/backend.solr.js | 4 +- src/model.js | 10 +- test/index.html | 1 + .../0.4.0/underscore.deferred.js | 445 ++++++++++++++++++ 14 files changed, 505 insertions(+), 48 deletions(-) create mode 100644 vendor/underscore.deferred/0.4.0/underscore.deferred.js diff --git a/README.md b/README.md index 6c70c5a1..61e1a7e1 100755 --- a/README.md +++ b/README.md @@ -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) diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index af8baeeb..3abbb84d 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -22,6 +22,7 @@ + diff --git a/download.markdown b/download.markdown index db8a7e67..8ad29dde 100644 --- a/download.markdown +++ b/download.markdown @@ -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/) >= 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/) >= 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/) >= 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/) >= 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 diff --git a/src/backend.ckan.js b/src/backend.ckan.js index 89a65a2c..dafe6ecd 100644 --- a/src/backend.ckan.js +++ b/src/backend.ckan.js @@ -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)); diff --git a/src/backend.couchdb.js b/src/backend.couchdb.js index 0ef3f625..8c2c9ad9 100755 --- a/src/backend.couchdb.js +++ b/src/backend.couchdb.js @@ -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)); \ No newline at end of file +}(jQuery, this.recline.Backend.CouchDB)); diff --git a/src/backend.csv.js b/src/backend.csv.js index 81b9771b..83e71d9c 100644 --- a/src/backend.csv.js +++ b/src/backend.csv.js @@ -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 || {}; // } // 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)); diff --git a/src/backend.dataproxy.js b/src/backend.dataproxy.js index 58f537d7..b8f17826 100644 --- a/src/backend.dataproxy.js +++ b/src/backend.dataproxy.js @@ -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)); diff --git a/src/backend.elasticsearch.js b/src/backend.elasticsearch.js index 9037917f..56075a3c 100644 --- a/src/backend.elasticsearch.js +++ b/src/backend.elasticsearch.js @@ -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) { diff --git a/src/backend.gdocs.js b/src/backend.gdocs.js index 4a36c302..3f1812a8 100644 --- a/src/backend.gdocs.js +++ b/src/backend.gdocs.js @@ -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)); diff --git a/src/backend.memory.js b/src/backend.memory.js index 6dedae38..07773e77 100644 --- a/src/backend.memory.js +++ b/src/backend.memory.js @@ -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)); diff --git a/src/backend.solr.js b/src/backend.solr.js index fae6d353..51b4ab71 100644 --- a/src/backend.solr.js +++ b/src/backend.solr.js @@ -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, diff --git a/src/model.js b/src/model.js index e912d8e5..e2217134 100644 --- a/src/model.js +++ b/src/model.js @@ -2,7 +2,7 @@ this.recline = this.recline || {}; this.recline.Model = this.recline.Model || {}; -(function($, my) { +(function(my) { // ## Dataset 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)); diff --git a/test/index.html b/test/index.html index 23900b7f..5d4831dd 100644 --- a/test/index.html +++ b/test/index.html @@ -10,6 +10,7 @@ + diff --git a/vendor/underscore.deferred/0.4.0/underscore.deferred.js b/vendor/underscore.deferred/0.4.0/underscore.deferred.js new file mode 100644 index 00000000..abb8bb0b --- /dev/null +++ b/vendor/underscore.deferred/0.4.0/underscore.deferred.js @@ -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);