rails-backbone-generator 0.0.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +18 -15
  3. data/lib/generators/backbone_generator.rb +1 -1
  4. data/lib/generators/{backbone → backbone_generator}/collection_generator.rb +9 -9
  5. data/lib/generators/{backbone → backbone_generator}/model_generator.rb +10 -10
  6. data/lib/generators/{backbone → backbone_generator}/namespace_generator.rb +18 -21
  7. data/lib/generators/{backbone → backbone_generator}/setup_generator.rb +25 -24
  8. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/Guardfile +4 -4
  9. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/app/assets/javascripts/application.js +7 -7
  10. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/app/assets/javascripts/shared/core_extentions/collections_extentions.coffee +8 -8
  11. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/app/assets/javascripts/shared/helpers/.gitkeep +0 -0
  12. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/app/assets/javascripts/shared/utils/.gitkeep +0 -0
  13. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/spec/javascripts/fixtures/.gitkeep +0 -0
  14. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/spec/javascripts/helpers/association_helpers.coffee +0 -0
  15. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/spec/javascripts/helpers/common_helpers.coffee +2 -2
  16. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/spec/javascripts/helpers/fake_host.coffee +3 -3
  17. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/spec/javascripts/helpers/headless_webkit_helper.coffee +0 -0
  18. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/spec/javascripts/support/jasmine.yml +0 -0
  19. data/lib/generators/backbone_generator/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-1.1.0.js +1581 -0
  20. data/lib/generators/{backbone/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-bind-to-1.0.0.coffee → backbone_generator/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-bind-to-1.1.0.coffee} +23 -12
  21. data/lib/generators/backbone_generator/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-handlebars-1.0.0.coffee +89 -0
  22. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-query-0.2.3.coffee +0 -0
  23. data/lib/generators/backbone_generator/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-relational-0.8.6.js +1943 -0
  24. data/lib/generators/backbone_generator/setup_generator/templates/vendor/assets/javascripts/backbone/handlebars-1.1.2.js +2595 -0
  25. data/lib/generators/{backbone/setup_generator/templates/vendor/assets/javascripts/backbone/underscore-1.3.3.js → backbone_generator/setup_generator/templates/vendor/assets/javascripts/backbone/underscore-1.5.2.js} +529 -312
  26. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/vendor/assets/javascripts/browser_compatibility/json2.js +5 -6
  27. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/vendor/assets/javascripts/browser_compatibility/localstorage-polyfill.js +12 -12
  28. data/lib/generators/{backbone/setup_generator/templates/vendor/assets/javascripts/jquery/jquery-1.8.0.js → backbone_generator/setup_generator/templates/vendor/assets/javascripts/jquery/jquery-1.10.2.js} +4633 -4071
  29. data/lib/generators/backbone_generator/setup_generator/templates/vendor/assets/javascripts/jquery/jquery-ui-1.10.3.js +15003 -0
  30. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/vendor/assets/javascripts/jquery/jquery.easing-1.3.js +33 -33
  31. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/vendor/assets/javascripts/jquery/jquery_ujs.js +162 -136
  32. data/lib/generators/{backbone → backbone_generator}/setup_generator/templates/vendor/assets/javascripts/testing/backbone-factory.js +4 -4
  33. data/lib/generators/backbone_generator/setup_generator/templates/vendor/assets/javascripts/testing/jasmine-jquery-1.5.93.js +700 -0
  34. data/lib/generators/backbone_generator/setup_generator/templates/vendor/assets/javascripts/testing/jasmine-sinon-0.2.1.js +43 -0
  35. data/lib/generators/backbone_generator/setup_generator/templates/vendor/assets/javascripts/testing/mock-ajax-2.0.0.js +264 -0
  36. data/lib/generators/{backbone/setup_generator/templates/vendor/assets/javascripts/testing/sinon-1.4.2.js → backbone_generator/setup_generator/templates/vendor/assets/javascripts/testing/sinon-1.7.3.js} +686 -477
  37. data/lib/generators/{backbone → backbone_generator}/templates/app/assets/javascripts/%namespace%.coffee.tt +0 -1
  38. data/lib/generators/{backbone → backbone_generator}/templates/app/assets/javascripts/%namespace%/collections/%collection_name%.coffee.tt +0 -0
  39. data/lib/generators/{backbone → backbone_generator}/templates/app/assets/javascripts/%namespace%/collections/.gitkeep +0 -0
  40. data/lib/generators/{backbone → backbone_generator}/templates/app/assets/javascripts/%namespace%/models/%model_name%.coffee.tt +2 -2
  41. data/lib/generators/{backbone → backbone_generator}/templates/app/assets/javascripts/%namespace%/models/.gitkeep +0 -0
  42. data/lib/generators/{backbone → backbone_generator}/templates/app/assets/javascripts/%namespace%/routes/.gitkeep +0 -0
  43. data/lib/generators/{backbone → backbone_generator}/templates/app/assets/javascripts/%namespace%/templates/.gitkeep +0 -0
  44. data/lib/generators/{backbone → backbone_generator}/templates/app/assets/javascripts/%namespace%/views/.gitkeep +0 -0
  45. data/lib/generators/{backbone → backbone_generator}/templates/spec/javascripts/%namespace%/collections/%collection_name%_spec.coffee.tt +1 -1
  46. data/lib/generators/{backbone → backbone_generator}/templates/spec/javascripts/%namespace%/collections/.gitkeep +0 -0
  47. data/lib/generators/{backbone → backbone_generator}/templates/spec/javascripts/%namespace%/factories/%model_name%_factory.coffee.tt +1 -1
  48. data/lib/generators/{backbone → backbone_generator}/templates/spec/javascripts/%namespace%/factories/.gitkeep +0 -0
  49. data/lib/generators/{backbone → backbone_generator}/templates/spec/javascripts/%namespace%/models/%model_name%_spec.coffee.tt +3 -4
  50. data/lib/generators/{backbone → backbone_generator}/templates/spec/javascripts/%namespace%/models/.gitkeep +0 -0
  51. data/lib/generators/{backbone → backbone_generator}/templates/spec/javascripts/%namespace%/views/.gitkeep +0 -0
  52. data/lib/rails_backbone_generator/version.rb +1 -1
  53. metadata +53 -59
  54. data/lib/generators/backbone/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-0.9.2.js +0 -1431
  55. data/lib/generators/backbone/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-handlebars-1.0.0.js +0 -121
  56. data/lib/generators/backbone/setup_generator/templates/vendor/assets/javascripts/backbone/backbone-relational-0.6.0.js +0 -1687
  57. data/lib/generators/backbone/setup_generator/templates/vendor/assets/javascripts/backbone/handlebars-1.0.rc.1.js +0 -1920
  58. data/lib/generators/backbone/setup_generator/templates/vendor/assets/javascripts/jquery/jquery-ui.min.js +0 -17
  59. data/lib/generators/backbone/setup_generator/templates/vendor/assets/javascripts/testing/jasmine-jquery-1.3.1.js +0 -288
  60. data/lib/generators/backbone/setup_generator/templates/vendor/assets/javascripts/testing/jasmine-sinon.js +0 -43
  61. data/lib/generators/backbone/setup_generator/templates/vendor/assets/javascripts/testing/mock-ajax.js +0 -207
@@ -1,5 +1,5 @@
1
1
  ###
2
- Backbone BindTo 1.0.0
2
+ Backbone.BindTo
3
3
 
4
4
  Author: Radoslav Stankov
5
5
  Project site: https://github.com/RStankov/backbone-bind-to
@@ -17,26 +17,37 @@ class BindToView extends BackboneView
17
17
  @bindTo @collection, eventName, methodName for eventName, methodName of @bindToCollection if @collection
18
18
 
19
19
  bindTo: (object, eventName, methodName) ->
20
- unless object in [@model, @collection]
21
- @_binded = []
22
- @_binded.push object unless _.include @_binded, object
20
+ callback = if typeof methodName is 'function' then methodName else @[methodName]
23
21
 
24
- throw new Error "Method #{methodName} does not exists" unless @[methodName]
25
- throw new Error "#{methodName} is not a function" unless typeof @[methodName] is 'function'
22
+ throw new Error "Method #{methodName} does not exists" unless callback
23
+ throw new Error "#{methodName} is not a function" unless typeof callback is 'function'
26
24
 
27
- object.on eventName, @[methodName], @
25
+ if object.on is Backbone.Events.on
26
+ @listenTo object, eventName, callback
27
+ else
28
+ @_binded ?= []
29
+ @_binded.push object
30
+ Backbone.$(object).on "#{eventName}.bindToEvent", _.bind(callback, this)
28
31
 
29
- remove: ->
30
- @model.off null, null, @ if @model
31
- @collection.off null, null, @ if @collection
32
+ this
33
+
34
+ unbindFromAll: ->
35
+ @model.off null, null, @ if @model and @model.off
36
+ @collection.off null, null, @ if @collection and @collection.off
32
37
 
33
- _.invoke @_binded, 'off', null, null, @
38
+ Backbone.$(element).off '.bindToEvent' for element in @_binded if @_binded
34
39
  delete @_binded
35
40
 
41
+ @stopListening()
42
+
43
+ this
44
+
45
+ remove: ->
46
+ @unbindFromAll()
36
47
  super
37
48
 
38
49
  Backbone.BindTo =
39
- VERSION: '1.0.0'
50
+ VERSION: '1.1.0'
40
51
 
41
52
  noConflict: ->
42
53
  root.Backbone.View = BackboneView
@@ -0,0 +1,89 @@
1
+ ###
2
+ Backbone Handlebars
3
+
4
+ Author: Radoslav Stankov
5
+ Project site: https://github.com/RStankov/backbone-handlebars
6
+ Licensed under the MIT License.
7
+ ###
8
+
9
+ BH =
10
+ VERSION: '1.0.0'
11
+
12
+ postponed: {}
13
+ rendered: {}
14
+
15
+ postponeRender: (name, options, parentView) ->
16
+ viewClass = _.inject (name || '').split('.'), ((memo, fragment) -> memo[fragment] || false), window
17
+ throw "Invalid view name - #{name}" unless viewClass
18
+
19
+ view = new viewClass options.hash
20
+ view.template = options.fn if options.fn?
21
+
22
+ cid = (parentView || options.data.view).cid
23
+
24
+ @postponed[cid] ?= []
25
+ @postponed[cid].push view
26
+
27
+ '<div id="_' + view.cid + '"></div>'
28
+
29
+ renderPostponed: (parentView) ->
30
+ cid = parentView.cid
31
+
32
+ @rendered[cid] = _.map @postponed[parentView.cid], (view) ->
33
+ view.render()
34
+ parentView.$("#_#{view.cid}").replaceWith view.el
35
+ view
36
+
37
+ delete @postponed[cid]
38
+
39
+ clearRendered: (parentView) ->
40
+ cid = parentView.cid
41
+
42
+ if @rendered[cid]
43
+ _.invoke @rendered[cid], 'remove'
44
+ delete @rendered[cid]
45
+
46
+ Handlebars.registerHelper 'view', (name, options) ->
47
+ new Handlebars.SafeString BH.postponeRender(name, options, @_parentView)
48
+
49
+ Handlebars.registerHelper 'views', (name, models, options) ->
50
+ callback = (model) =>
51
+ options.hash.model = model
52
+ BH.postponeRender name, options, @_parentView
53
+
54
+ markers = if 'map' of models
55
+ models.map callback
56
+ else
57
+ _.map callback
58
+
59
+ new Handlebars.SafeString markers.join('')
60
+
61
+ _compile = Handlebars.compile
62
+ Handlebars.compile = (template, options = {}) ->
63
+ options.data = true
64
+ _compile.call this, template, options
65
+
66
+ Backbone.View::renderTemplate = (context = {}) ->
67
+ BH.clearRendered this
68
+ context = _.clone context
69
+ context._parentView = this
70
+ @$el.html @template context, data: {view: this}
71
+ BH.renderPostponed this
72
+ this
73
+
74
+ Backbone.View::renderedSubViews = ->
75
+ BH.rendered[@cid]
76
+
77
+ _remove = Backbone.View::remove
78
+ Backbone.View::remove = ->
79
+ BH.clearRendered this
80
+ _remove.apply this, arguments
81
+
82
+ Backbone.View::render = ->
83
+ if @template
84
+ @renderTemplate if typeof @templateData is 'function' then @templateData() else @templateData
85
+ this
86
+
87
+ Backbone.View::templateData = -> {}
88
+
89
+ Backbone.Handlebars = BH
@@ -0,0 +1,1943 @@
1
+ /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
2
+ /**
3
+ * Backbone-relational.js 0.8.6
4
+ * (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors)
5
+ *
6
+ * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.
7
+ * For details and documentation: https://github.com/PaulUithol/Backbone-relational.
8
+ * Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone.
9
+ */
10
+ ( function( undefined ) {
11
+ "use strict";
12
+
13
+ /**
14
+ * CommonJS shim
15
+ **/
16
+ var _, Backbone, exports;
17
+ if ( typeof window === 'undefined' ) {
18
+ _ = require( 'underscore' );
19
+ Backbone = require( 'backbone' );
20
+ exports = Backbone;
21
+ typeof module === 'undefined' || ( module.exports = exports );
22
+ }
23
+ else {
24
+ _ = window._;
25
+ Backbone = window.Backbone;
26
+ exports = window;
27
+ }
28
+
29
+ Backbone.Relational = {
30
+ showWarnings: true
31
+ };
32
+
33
+ /**
34
+ * Semaphore mixin; can be used as both binary and counting.
35
+ **/
36
+ Backbone.Semaphore = {
37
+ _permitsAvailable: null,
38
+ _permitsUsed: 0,
39
+
40
+ acquire: function() {
41
+ if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) {
42
+ throw new Error( 'Max permits acquired' );
43
+ }
44
+ else {
45
+ this._permitsUsed++;
46
+ }
47
+ },
48
+
49
+ release: function() {
50
+ if ( this._permitsUsed === 0 ) {
51
+ throw new Error( 'All permits released' );
52
+ }
53
+ else {
54
+ this._permitsUsed--;
55
+ }
56
+ },
57
+
58
+ isLocked: function() {
59
+ return this._permitsUsed > 0;
60
+ },
61
+
62
+ setAvailablePermits: function( amount ) {
63
+ if ( this._permitsUsed > amount ) {
64
+ throw new Error( 'Available permits cannot be less than used permits' );
65
+ }
66
+ this._permitsAvailable = amount;
67
+ }
68
+ };
69
+
70
+ /**
71
+ * A BlockingQueue that accumulates items while blocked (via 'block'),
72
+ * and processes them when unblocked (via 'unblock').
73
+ * Process can also be called manually (via 'process').
74
+ */
75
+ Backbone.BlockingQueue = function() {
76
+ this._queue = [];
77
+ };
78
+ _.extend( Backbone.BlockingQueue.prototype, Backbone.Semaphore, {
79
+ _queue: null,
80
+
81
+ add: function( func ) {
82
+ if ( this.isBlocked() ) {
83
+ this._queue.push( func );
84
+ }
85
+ else {
86
+ func();
87
+ }
88
+ },
89
+
90
+ // Some of the queued events may trigger other blocking events. By
91
+ // copying the queue here it allows queued events to process closer to
92
+ // the natural order.
93
+ //
94
+ // queue events [ 'A', 'B', 'C' ]
95
+ // A handler of 'B' triggers 'D' and 'E'
96
+ // By copying `this._queue` this executes:
97
+ // [ 'A', 'B', 'D', 'E', 'C' ]
98
+ // The same order the would have executed if they didn't have to be
99
+ // delayed and queued.
100
+ process: function() {
101
+ var queue = this._queue;
102
+ this._queue = [];
103
+ while ( queue && queue.length ) {
104
+ queue.shift()();
105
+ }
106
+ },
107
+
108
+ block: function() {
109
+ this.acquire();
110
+ },
111
+
112
+ unblock: function() {
113
+ this.release();
114
+ if ( !this.isBlocked() ) {
115
+ this.process();
116
+ }
117
+ },
118
+
119
+ isBlocked: function() {
120
+ return this.isLocked();
121
+ }
122
+ });
123
+ /**
124
+ * Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'change:<key>')
125
+ * until the top-level object is fully initialized (see 'Backbone.RelationalModel').
126
+ */
127
+ Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
128
+
129
+ /**
130
+ * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.
131
+ * Handles lookup for relations.
132
+ */
133
+ Backbone.Store = function() {
134
+ this._collections = [];
135
+ this._reverseRelations = [];
136
+ this._orphanRelations = [];
137
+ this._subModels = [];
138
+ this._modelScopes = [ exports ];
139
+ };
140
+ _.extend( Backbone.Store.prototype, Backbone.Events, {
141
+ /**
142
+ * Create a new `Relation`.
143
+ * @param {Backbone.RelationalModel} [model]
144
+ * @param {Object} relation
145
+ * @param {Object} [options]
146
+ */
147
+ initializeRelation: function( model, relation, options ) {
148
+ var type = !_.isString( relation.type ) ? relation.type : Backbone[ relation.type ] || this.getObjectByName( relation.type );
149
+ if ( type && type.prototype instanceof Backbone.Relation ) {
150
+ new type( model, relation, options ); // Also pushes the new Relation into `model._relations`
151
+ }
152
+ else {
153
+ Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid relation type!', relation );
154
+ }
155
+ },
156
+
157
+ /**
158
+ * Add a scope for `getObjectByName` to look for model types by name.
159
+ * @param {Object} scope
160
+ */
161
+ addModelScope: function( scope ) {
162
+ this._modelScopes.push( scope );
163
+ },
164
+
165
+ /**
166
+ * Remove a scope.
167
+ * @param {Object} scope
168
+ */
169
+ removeModelScope: function( scope ) {
170
+ this._modelScopes = _.without( this._modelScopes, scope );
171
+ },
172
+
173
+ /**
174
+ * Add a set of subModelTypes to the store, that can be used to resolve the '_superModel'
175
+ * for a model later in 'setupSuperModel'.
176
+ *
177
+ * @param {Backbone.RelationalModel} subModelTypes
178
+ * @param {Backbone.RelationalModel} superModelType
179
+ */
180
+ addSubModels: function( subModelTypes, superModelType ) {
181
+ this._subModels.push({
182
+ 'superModelType': superModelType,
183
+ 'subModels': subModelTypes
184
+ });
185
+ },
186
+
187
+ /**
188
+ * Check if the given modelType is registered as another model's subModel. If so, add it to the super model's
189
+ * '_subModels', and set the modelType's '_superModel', '_subModelTypeName', and '_subModelTypeAttribute'.
190
+ *
191
+ * @param {Backbone.RelationalModel} modelType
192
+ */
193
+ setupSuperModel: function( modelType ) {
194
+ _.find( this._subModels, function( subModelDef ) {
195
+ return _.find( subModelDef.subModels || [], function( subModelTypeName, typeValue ) {
196
+ var subModelType = this.getObjectByName( subModelTypeName );
197
+
198
+ if ( modelType === subModelType ) {
199
+ // Set 'modelType' as a child of the found superModel
200
+ subModelDef.superModelType._subModels[ typeValue ] = modelType;
201
+
202
+ // Set '_superModel', '_subModelTypeValue', and '_subModelTypeAttribute' on 'modelType'.
203
+ modelType._superModel = subModelDef.superModelType;
204
+ modelType._subModelTypeValue = typeValue;
205
+ modelType._subModelTypeAttribute = subModelDef.superModelType.prototype.subModelTypeAttribute;
206
+ return true;
207
+ }
208
+ }, this );
209
+ }, this );
210
+ },
211
+
212
+ /**
213
+ * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
214
+ * existing instances of 'model' in the store as well.
215
+ * @param {Object} relation
216
+ * @param {Backbone.RelationalModel} relation.model
217
+ * @param {String} relation.type
218
+ * @param {String} relation.key
219
+ * @param {String|Object} relation.relatedModel
220
+ */
221
+ addReverseRelation: function( relation ) {
222
+ var exists = _.any( this._reverseRelations, function( rel ) {
223
+ return _.all( relation || [], function( val, key ) {
224
+ return val === rel[ key ];
225
+ });
226
+ });
227
+
228
+ if ( !exists && relation.model && relation.type ) {
229
+ this._reverseRelations.push( relation );
230
+ this._addRelation( relation.model, relation );
231
+ this.retroFitRelation( relation );
232
+ }
233
+ },
234
+
235
+ /**
236
+ * Deposit a `relation` for which the `relatedModel` can't be resolved at the moment.
237
+ *
238
+ * @param {Object} relation
239
+ */
240
+ addOrphanRelation: function( relation ) {
241
+ var exists = _.any( this._orphanRelations, function( rel ) {
242
+ return _.all( relation || [], function( val, key ) {
243
+ return val === rel[ key ];
244
+ });
245
+ });
246
+
247
+ if ( !exists && relation.model && relation.type ) {
248
+ this._orphanRelations.push( relation );
249
+ }
250
+ },
251
+
252
+ /**
253
+ * Try to initialize any `_orphanRelation`s
254
+ */
255
+ processOrphanRelations: function() {
256
+ // Make sure to operate on a copy since we're removing while iterating
257
+ _.each( this._orphanRelations.slice( 0 ), function( rel ) {
258
+ var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
259
+ if ( relatedModel ) {
260
+ this.initializeRelation( null, rel );
261
+ this._orphanRelations = _.without( this._orphanRelations, rel );
262
+ }
263
+ }, this );
264
+ },
265
+
266
+ /**
267
+ *
268
+ * @param {Backbone.RelationalModel.constructor} type
269
+ * @param {Object} relation
270
+ * @private
271
+ */
272
+ _addRelation: function( type, relation ) {
273
+ if ( !type.prototype.relations ) {
274
+ type.prototype.relations = [];
275
+ }
276
+ type.prototype.relations.push( relation );
277
+
278
+ _.each( type._subModels || [], function( subModel ) {
279
+ this._addRelation( subModel, relation );
280
+ }, this );
281
+ },
282
+
283
+ /**
284
+ * Add a 'relation' to all existing instances of 'relation.model' in the store
285
+ * @param {Object} relation
286
+ */
287
+ retroFitRelation: function( relation ) {
288
+ var coll = this.getCollection( relation.model, false );
289
+ coll && coll.each( function( model ) {
290
+ if ( !( model instanceof relation.model ) ) {
291
+ return;
292
+ }
293
+
294
+ new relation.type( model, relation );
295
+ }, this );
296
+ },
297
+
298
+ /**
299
+ * Find the Store's collection for a certain type of model.
300
+ * @param {Backbone.RelationalModel} type
301
+ * @param {Boolean} [create=true] Should a collection be created if none is found?
302
+ * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
303
+ */
304
+ getCollection: function( type, create ) {
305
+ if ( type instanceof Backbone.RelationalModel ) {
306
+ type = type.constructor;
307
+ }
308
+
309
+ var rootModel = type;
310
+ while ( rootModel._superModel ) {
311
+ rootModel = rootModel._superModel;
312
+ }
313
+
314
+ var coll = _.find( this._collections, function(item) {
315
+ return item.model === rootModel;
316
+ });
317
+
318
+ if ( !coll && create !== false ) {
319
+ coll = this._createCollection( rootModel );
320
+ }
321
+
322
+ return coll;
323
+ },
324
+
325
+ /**
326
+ * Find a model type on one of the modelScopes by name. Names are split on dots.
327
+ * @param {String} name
328
+ * @return {Object}
329
+ */
330
+ getObjectByName: function( name ) {
331
+ var parts = name.split( '.' ),
332
+ type = null;
333
+
334
+ _.find( this._modelScopes, function( scope ) {
335
+ type = _.reduce( parts || [], function( memo, val ) {
336
+ return memo ? memo[ val ] : undefined;
337
+ }, scope );
338
+
339
+ if ( type && type !== scope ) {
340
+ return true;
341
+ }
342
+ }, this );
343
+
344
+ return type;
345
+ },
346
+
347
+ _createCollection: function( type ) {
348
+ var coll;
349
+
350
+ // If 'type' is an instance, take its constructor
351
+ if ( type instanceof Backbone.RelationalModel ) {
352
+ type = type.constructor;
353
+ }
354
+
355
+ // Type should inherit from Backbone.RelationalModel.
356
+ if ( type.prototype instanceof Backbone.RelationalModel ) {
357
+ coll = new Backbone.Collection();
358
+ coll.model = type;
359
+
360
+ this._collections.push( coll );
361
+ }
362
+
363
+ return coll;
364
+ },
365
+
366
+ /**
367
+ * Find the attribute that is to be used as the `id` on a given object
368
+ * @param type
369
+ * @param {String|Number|Object|Backbone.RelationalModel} item
370
+ * @return {String|Number}
371
+ */
372
+ resolveIdForItem: function( type, item ) {
373
+ var id = _.isString( item ) || _.isNumber( item ) ? item : null;
374
+
375
+ if ( id === null ) {
376
+ if ( item instanceof Backbone.RelationalModel ) {
377
+ id = item.id;
378
+ }
379
+ else if ( _.isObject( item ) ) {
380
+ id = item[ type.prototype.idAttribute ];
381
+ }
382
+ }
383
+
384
+ // Make all falsy values `null` (except for 0, which could be an id.. see '/issues/179')
385
+ if ( !id && id !== 0 ) {
386
+ id = null;
387
+ }
388
+
389
+ return id;
390
+ },
391
+
392
+ /**
393
+ * Find a specific model of a certain `type` in the store
394
+ * @param type
395
+ * @param {String|Number|Object|Backbone.RelationalModel} item
396
+ */
397
+ find: function( type, item ) {
398
+ var id = this.resolveIdForItem( type, item );
399
+ var coll = this.getCollection( type );
400
+
401
+ // Because the found object could be of any of the type's superModel
402
+ // types, only return it if it's actually of the type asked for.
403
+ if ( coll ) {
404
+ var obj = coll.get( id );
405
+
406
+ if ( obj instanceof type ) {
407
+ return obj;
408
+ }
409
+ }
410
+
411
+ return null;
412
+ },
413
+
414
+ /**
415
+ * Add a 'model' to its appropriate collection. Retain the original contents of 'model.collection'.
416
+ * @param {Backbone.RelationalModel} model
417
+ */
418
+ register: function( model ) {
419
+ var coll = this.getCollection( model );
420
+
421
+ if ( coll ) {
422
+ var modelColl = model.collection;
423
+ coll.add( model );
424
+ this.listenTo( model, 'destroy', this.unregister, this );
425
+ this.listenTo( model, 'relational:unregister', this.unregister, this );
426
+ model.collection = modelColl;
427
+ }
428
+ },
429
+
430
+ /**
431
+ * Check if the given model may use the given `id`
432
+ * @param model
433
+ * @param [id]
434
+ */
435
+ checkId: function( model, id ) {
436
+ var coll = this.getCollection( model ),
437
+ duplicate = coll && coll.get( id );
438
+
439
+ if ( duplicate && model !== duplicate ) {
440
+ if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
441
+ console.warn( 'Duplicate id! Old RelationalModel=%o, new RelationalModel=%o', duplicate, model );
442
+ }
443
+
444
+ throw new Error( "Cannot instantiate more than one Backbone.RelationalModel with the same id per type!" );
445
+ }
446
+ },
447
+
448
+ /**
449
+ * Explicitly update a model's id in its store collection
450
+ * @param {Backbone.RelationalModel} model
451
+ */
452
+ update: function( model ) {
453
+ var coll = this.getCollection( model );
454
+ // This triggers updating the lookup indices kept in a collection
455
+ coll._onModelEvent( 'change:' + model.idAttribute, model, coll );
456
+
457
+ // Trigger an event on model so related models (having the model's new id in their keyContents) can add it.
458
+ model.trigger( 'relational:change:id', model, coll );
459
+ },
460
+
461
+ /**
462
+ * Remove a 'model' from the store.
463
+ * @param {Backbone.RelationalModel} model
464
+ * @param {Backbone.Collection} [collection]
465
+ * @param {Object} [options]
466
+ */
467
+ unregister: function( model, collection, options ) {
468
+ this.stopListening( model );
469
+ var coll = this.getCollection( model );
470
+ coll && coll.remove( model, options );
471
+ },
472
+
473
+ /**
474
+ * Reset the `store` to it's original state. The `reverseRelations` are kept though, since attempting to
475
+ * re-initialize these on models would lead to a large amount of warnings.
476
+ */
477
+ reset: function() {
478
+ this.stopListening();
479
+ this._collections = [];
480
+ this._subModels = [];
481
+ this._modelScopes = [ exports ];
482
+ }
483
+ });
484
+ Backbone.Relational.store = new Backbone.Store();
485
+
486
+ /**
487
+ * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:<key>' events
488
+ * are used to regulate addition and removal of models from relations.
489
+ *
490
+ * @param {Backbone.RelationalModel} [instance] Model that this relation is created for. If no model is supplied,
491
+ * Relation just tries to instantiate it's `reverseRelation` if specified, and bails out after that.
492
+ * @param {Object} options
493
+ * @param {string} options.key
494
+ * @param {Backbone.RelationalModel.constructor} options.relatedModel
495
+ * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids.
496
+ * @param {Boolean} [options.createModels=true] Create objects from the contents of keys if the object is not found in Backbone.store.
497
+ * @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate
498
+ * the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs
499
+ * {Backbone.Relation|String} type ('HasOne' or 'HasMany').
500
+ * @param {Object} opts
501
+ */
502
+ Backbone.Relation = function( instance, options, opts ) {
503
+ this.instance = instance;
504
+ // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
505
+ options = _.isObject( options ) ? options : {};
506
+ this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
507
+ this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
508
+
509
+ this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
510
+ Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
511
+
512
+ this.key = this.options.key;
513
+ this.keySource = this.options.keySource || this.key;
514
+ this.keyDestination = this.options.keyDestination || this.keySource || this.key;
515
+
516
+ this.model = this.options.model || this.instance.constructor;
517
+
518
+ this.relatedModel = this.options.relatedModel;
519
+
520
+ if ( _.isFunction( this.relatedModel ) && !( this.relatedModel.prototype instanceof Backbone.RelationalModel ) ) {
521
+ this.relatedModel = _.result( this, 'relatedModel' );
522
+ }
523
+ if ( _.isString( this.relatedModel ) ) {
524
+ this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel );
525
+ }
526
+
527
+ if ( !this.checkPreconditions() ) {
528
+ return;
529
+ }
530
+
531
+ // Add the reverse relation on 'relatedModel' to the store's reverseRelations
532
+ if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
533
+ Backbone.Relational.store.addReverseRelation( _.defaults( {
534
+ isAutoRelation: true,
535
+ model: this.relatedModel,
536
+ relatedModel: this.model,
537
+ reverseRelation: this.options // current relation is the 'reverseRelation' for its own reverseRelation
538
+ },
539
+ this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
540
+ ) );
541
+ }
542
+
543
+ if ( instance ) {
544
+ var contentKey = this.keySource;
545
+ if ( contentKey !== this.key && typeof this.instance.get( this.key ) === 'object' ) {
546
+ contentKey = this.key;
547
+ }
548
+
549
+ this.setKeyContents( this.instance.get( contentKey ) );
550
+ this.relatedCollection = Backbone.Relational.store.getCollection( this.relatedModel );
551
+
552
+ // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
553
+ if ( this.keySource !== this.key ) {
554
+ delete this.instance.attributes[ this.keySource ];
555
+ }
556
+
557
+ // Add this Relation to instance._relations
558
+ this.instance._relations[ this.key ] = this;
559
+
560
+ this.initialize( opts );
561
+
562
+ if ( this.options.autoFetch ) {
563
+ this.instance.fetchRelated( this.key, _.isObject( this.options.autoFetch ) ? this.options.autoFetch : {} );
564
+ }
565
+
566
+ // When 'relatedModel' are created or destroyed, check if it affects this relation.
567
+ this.listenTo( this.instance, 'destroy', this.destroy )
568
+ .listenTo( this.relatedCollection, 'relational:add relational:change:id', this.tryAddRelated )
569
+ .listenTo( this.relatedCollection, 'relational:remove', this.removeRelated )
570
+ }
571
+ };
572
+ // Fix inheritance :\
573
+ Backbone.Relation.extend = Backbone.Model.extend;
574
+ // Set up all inheritable **Backbone.Relation** properties and methods.
575
+ _.extend( Backbone.Relation.prototype, Backbone.Events, Backbone.Semaphore, {
576
+ options: {
577
+ createModels: true,
578
+ includeInJSON: true,
579
+ isAutoRelation: false,
580
+ autoFetch: false,
581
+ parse: false
582
+ },
583
+
584
+ instance: null,
585
+ key: null,
586
+ keyContents: null,
587
+ relatedModel: null,
588
+ relatedCollection: null,
589
+ reverseRelation: null,
590
+ related: null,
591
+
592
+ /**
593
+ * Check several pre-conditions.
594
+ * @return {Boolean} True if pre-conditions are satisfied, false if they're not.
595
+ */
596
+ checkPreconditions: function() {
597
+ var i = this.instance,
598
+ k = this.key,
599
+ m = this.model,
600
+ rm = this.relatedModel,
601
+ warn = Backbone.Relational.showWarnings && typeof console !== 'undefined';
602
+
603
+ if ( !m || !k || !rm ) {
604
+ warn && console.warn( 'Relation=%o: missing model, key or relatedModel (%o, %o, %o).', this, m, k, rm );
605
+ return false;
606
+ }
607
+ // Check if the type in 'model' inherits from Backbone.RelationalModel
608
+ if ( !( m.prototype instanceof Backbone.RelationalModel ) ) {
609
+ warn && console.warn( 'Relation=%o: model does not inherit from Backbone.RelationalModel (%o).', this, i );
610
+ return false;
611
+ }
612
+ // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
613
+ if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) {
614
+ warn && console.warn( 'Relation=%o: relatedModel does not inherit from Backbone.RelationalModel (%o).', this, rm );
615
+ return false;
616
+ }
617
+ // Check if this is not a HasMany, and the reverse relation is HasMany as well
618
+ if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) {
619
+ warn && console.warn( 'Relation=%o: relation is a HasMany, and the reverseRelation is HasMany as well.', this );
620
+ return false;
621
+ }
622
+ // Check if we're not attempting to create a relationship on a `key` that's already used.
623
+ if ( i && _.keys( i._relations ).length ) {
624
+ var existing = _.find( i._relations, function( rel ) {
625
+ return rel.key === k;
626
+ }, this );
627
+
628
+ if ( existing ) {
629
+ warn && console.warn( 'Cannot create relation=%o on %o for model=%o: already taken by relation=%o.',
630
+ this, k, i, existing );
631
+ return false;
632
+ }
633
+ }
634
+
635
+ return true;
636
+ },
637
+
638
+ /**
639
+ * Set the related model(s) for this relation
640
+ * @param {Backbone.Model|Backbone.Collection} related
641
+ */
642
+ setRelated: function( related ) {
643
+ this.related = related;
644
+
645
+ this.instance.acquire();
646
+ this.instance.attributes[ this.key ] = related;
647
+ this.instance.release();
648
+ },
649
+
650
+ /**
651
+ * Determine if a relation (on a different RelationalModel) is the reverse
652
+ * relation of the current one.
653
+ * @param {Backbone.Relation} relation
654
+ * @return {Boolean}
655
+ */
656
+ _isReverseRelation: function( relation ) {
657
+ return relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
658
+ this.key === relation.reverseRelation.key;
659
+ },
660
+
661
+ /**
662
+ * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).
663
+ * @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model.
664
+ * If not specified, 'this.related' is used.
665
+ * @return {Backbone.Relation[]}
666
+ */
667
+ getReverseRelations: function( model ) {
668
+ var reverseRelations = [];
669
+ // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array.
670
+ var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] );
671
+ _.each( models || [], function( related ) {
672
+ _.each( related.getRelations() || [], function( relation ) {
673
+ if ( this._isReverseRelation( relation ) ) {
674
+ reverseRelations.push( relation );
675
+ }
676
+ }, this );
677
+ }, this );
678
+
679
+ return reverseRelations;
680
+ },
681
+
682
+ /**
683
+ * When `this.instance` is destroyed, cleanup our relations.
684
+ * Get reverse relation, call removeRelated on each.
685
+ */
686
+ destroy: function() {
687
+ this.stopListening();
688
+
689
+ if ( this instanceof Backbone.HasOne ) {
690
+ this.setRelated( null );
691
+ }
692
+ else if ( this instanceof Backbone.HasMany ) {
693
+ this.setRelated( this._prepareCollection() );
694
+ }
695
+
696
+ _.each( this.getReverseRelations(), function( relation ) {
697
+ relation.removeRelated( this.instance );
698
+ }, this );
699
+ }
700
+ });
701
+
702
+ Backbone.HasOne = Backbone.Relation.extend({
703
+ options: {
704
+ reverseRelation: { type: 'HasMany' }
705
+ },
706
+
707
+ initialize: function( opts ) {
708
+ this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
709
+
710
+ var related = this.findRelated( opts );
711
+ this.setRelated( related );
712
+
713
+ // Notify new 'related' object of the new relation.
714
+ _.each( this.getReverseRelations(), function( relation ) {
715
+ relation.addRelated( this.instance, opts );
716
+ }, this );
717
+ },
718
+
719
+ /**
720
+ * Find related Models.
721
+ * @param {Object} [options]
722
+ * @return {Backbone.Model}
723
+ */
724
+ findRelated: function( options ) {
725
+ var related = null;
726
+
727
+ options = _.defaults( { parse: this.options.parse }, options );
728
+
729
+ if ( this.keyContents instanceof this.relatedModel ) {
730
+ related = this.keyContents;
731
+ }
732
+ else if ( this.keyContents || this.keyContents === 0 ) { // since 0 can be a valid `id` as well
733
+ var opts = _.defaults( { create: this.options.createModels }, options );
734
+ related = this.relatedModel.findOrCreate( this.keyContents, opts );
735
+ }
736
+
737
+ // Nullify `keyId` if we have a related model; in case it was already part of the relation
738
+ if ( related ) {
739
+ this.keyId = null;
740
+ }
741
+
742
+ return related;
743
+ },
744
+
745
+ /**
746
+ * Normalize and reduce `keyContents` to an `id`, for easier comparison
747
+ * @param {String|Number|Backbone.Model} keyContents
748
+ */
749
+ setKeyContents: function( keyContents ) {
750
+ this.keyContents = keyContents;
751
+ this.keyId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, this.keyContents );
752
+ },
753
+
754
+ /**
755
+ * Event handler for `change:<key>`.
756
+ * If the key is changed, notify old & new reverse relations and initialize the new relation.
757
+ */
758
+ onChange: function( model, attr, options ) {
759
+ // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)
760
+ if ( this.isLocked() ) {
761
+ return;
762
+ }
763
+ this.acquire();
764
+ options = options ? _.clone( options ) : {};
765
+
766
+ // 'options.__related' is set by 'addRelated'/'removeRelated'. If it is set, the change
767
+ // is the result of a call from a relation. If it's not, the change is the result of
768
+ // a 'set' call on this.instance.
769
+ var changed = _.isUndefined( options.__related ),
770
+ oldRelated = changed ? this.related : options.__related;
771
+
772
+ if ( changed ) {
773
+ this.setKeyContents( attr );
774
+ var related = this.findRelated( options );
775
+ this.setRelated( related );
776
+ }
777
+
778
+ // Notify old 'related' object of the terminated relation
779
+ if ( oldRelated && this.related !== oldRelated ) {
780
+ _.each( this.getReverseRelations( oldRelated ), function( relation ) {
781
+ relation.removeRelated( this.instance, null, options );
782
+ }, this );
783
+ }
784
+
785
+ // Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated;
786
+ // that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'.
787
+ // In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet.
788
+ _.each( this.getReverseRelations(), function( relation ) {
789
+ relation.addRelated( this.instance, options );
790
+ }, this );
791
+
792
+ // Fire the 'change:<key>' event if 'related' was updated
793
+ if ( !options.silent && this.related !== oldRelated ) {
794
+ var dit = this;
795
+ this.changed = true;
796
+ Backbone.Relational.eventQueue.add( function() {
797
+ dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
798
+ dit.changed = false;
799
+ });
800
+ }
801
+ this.release();
802
+ },
803
+
804
+ /**
805
+ * If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents'
806
+ */
807
+ tryAddRelated: function( model, coll, options ) {
808
+ if ( ( this.keyId || this.keyId === 0 ) && model.id === this.keyId ) { // since 0 can be a valid `id` as well
809
+ this.addRelated( model, options );
810
+ this.keyId = null;
811
+ }
812
+ },
813
+
814
+ addRelated: function( model, options ) {
815
+ // Allow 'model' to set up its relations before proceeding.
816
+ // (which can result in a call to 'addRelated' from a relation of 'model')
817
+ var dit = this;
818
+ model.queue( function() {
819
+ if ( model !== dit.related ) {
820
+ var oldRelated = dit.related || null;
821
+ dit.setRelated( model );
822
+ dit.onChange( dit.instance, model, _.defaults( { __related: oldRelated }, options ) );
823
+ }
824
+ });
825
+ },
826
+
827
+ removeRelated: function( model, coll, options ) {
828
+ if ( !this.related ) {
829
+ return;
830
+ }
831
+
832
+ if ( model === this.related ) {
833
+ var oldRelated = this.related || null;
834
+ this.setRelated( null );
835
+ this.onChange( this.instance, model, _.defaults( { __related: oldRelated }, options ) );
836
+ }
837
+ }
838
+ });
839
+
840
+ Backbone.HasMany = Backbone.Relation.extend({
841
+ collectionType: null,
842
+
843
+ options: {
844
+ reverseRelation: { type: 'HasOne' },
845
+ collectionType: Backbone.Collection,
846
+ collectionKey: true,
847
+ collectionOptions: {}
848
+ },
849
+
850
+ initialize: function( opts ) {
851
+ this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
852
+
853
+ // Handle a custom 'collectionType'
854
+ this.collectionType = this.options.collectionType;
855
+ if ( _.isFunction( this.collectionType ) && this.collectionType !== Backbone.Collection && !( this.collectionType.prototype instanceof Backbone.Collection ) ) {
856
+ this.collectionType = _.result( this, 'collectionType' );
857
+ }
858
+ if ( _.isString( this.collectionType ) ) {
859
+ this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType );
860
+ }
861
+ if ( this.collectionType !== Backbone.Collection && !( this.collectionType.prototype instanceof Backbone.Collection ) ) {
862
+ throw new Error( '`collectionType` must inherit from Backbone.Collection' );
863
+ }
864
+
865
+ var related = this.findRelated( opts );
866
+ this.setRelated( related );
867
+ },
868
+
869
+ /**
870
+ * Bind events and setup collectionKeys for a collection that is to be used as the backing store for a HasMany.
871
+ * If no 'collection' is supplied, a new collection will be created of the specified 'collectionType' option.
872
+ * @param {Backbone.Collection} [collection]
873
+ * @return {Backbone.Collection}
874
+ */
875
+ _prepareCollection: function( collection ) {
876
+ if ( this.related ) {
877
+ this.stopListening( this.related );
878
+ }
879
+
880
+ if ( !collection || !( collection instanceof Backbone.Collection ) ) {
881
+ var options = _.isFunction( this.options.collectionOptions ) ?
882
+ this.options.collectionOptions( this.instance ) : this.options.collectionOptions;
883
+
884
+ collection = new this.collectionType( null, options );
885
+ }
886
+
887
+ collection.model = this.relatedModel;
888
+
889
+ if ( this.options.collectionKey ) {
890
+ var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey;
891
+
892
+ if ( collection[ key ] && collection[ key ] !== this.instance ) {
893
+ if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
894
+ console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey );
895
+ }
896
+ }
897
+ else if ( key ) {
898
+ collection[ key ] = this.instance;
899
+ }
900
+ }
901
+
902
+ this.listenTo( collection, 'relational:add', this.handleAddition )
903
+ .listenTo( collection, 'relational:remove', this.handleRemoval )
904
+ .listenTo( collection, 'relational:reset', this.handleReset );
905
+
906
+ return collection;
907
+ },
908
+
909
+ /**
910
+ * Find related Models.
911
+ * @param {Object} [options]
912
+ * @return {Backbone.Collection}
913
+ */
914
+ findRelated: function( options ) {
915
+ var related = null;
916
+
917
+ options = _.defaults( { parse: this.options.parse }, options );
918
+
919
+ // Replace 'this.related' by 'this.keyContents' if it is a Backbone.Collection
920
+ if ( this.keyContents instanceof Backbone.Collection ) {
921
+ this._prepareCollection( this.keyContents );
922
+ related = this.keyContents;
923
+ }
924
+ // Otherwise, 'this.keyContents' should be an array of related object ids.
925
+ // Re-use the current 'this.related' if it is a Backbone.Collection; otherwise, create a new collection.
926
+ else {
927
+ var toAdd = [];
928
+
929
+ _.each( this.keyContents, function( attributes ) {
930
+ if ( attributes instanceof this.relatedModel ) {
931
+ var model = attributes;
932
+ }
933
+ else {
934
+ // If `merge` is true, update models here, instead of during update.
935
+ model = this.relatedModel.findOrCreate( attributes,
936
+ _.extend( { merge: true }, options, { create: this.options.createModels } )
937
+ );
938
+ }
939
+
940
+ model && toAdd.push( model );
941
+ }, this );
942
+
943
+ if ( this.related instanceof Backbone.Collection ) {
944
+ related = this.related;
945
+ }
946
+ else {
947
+ related = this._prepareCollection();
948
+ }
949
+
950
+ // By now, both `merge` and `parse` will already have been executed for models if they were specified.
951
+ // Disable them to prevent additional calls.
952
+ related.set( toAdd, _.defaults( { merge: false, parse: false }, options ) );
953
+ }
954
+
955
+ // Remove entries from `keyIds` that were already part of the relation (and are thus 'unchanged')
956
+ this.keyIds = _.difference( this.keyIds, _.pluck( related.models, 'id' ) );
957
+
958
+ return related;
959
+ },
960
+
961
+ /**
962
+ * Normalize and reduce `keyContents` to a list of `ids`, for easier comparison
963
+ * @param {String|Number|String[]|Number[]|Backbone.Collection} keyContents
964
+ */
965
+ setKeyContents: function( keyContents ) {
966
+ this.keyContents = keyContents instanceof Backbone.Collection ? keyContents : null;
967
+ this.keyIds = [];
968
+
969
+ if ( !this.keyContents && ( keyContents || keyContents === 0 ) ) { // since 0 can be a valid `id` as well
970
+ // Handle cases the an API/user supplies just an Object/id instead of an Array
971
+ this.keyContents = _.isArray( keyContents ) ? keyContents : [ keyContents ];
972
+
973
+ _.each( this.keyContents, function( item ) {
974
+ var itemId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
975
+ if ( itemId || itemId === 0 ) {
976
+ this.keyIds.push( itemId );
977
+ }
978
+ }, this );
979
+ }
980
+ },
981
+
982
+ /**
983
+ * Event handler for `change:<key>`.
984
+ * If the contents of the key are changed, notify old & new reverse relations and initialize the new relation.
985
+ */
986
+ onChange: function( model, attr, options ) {
987
+ options = options ? _.clone( options ) : {};
988
+ this.setKeyContents( attr );
989
+ this.changed = false;
990
+
991
+ var related = this.findRelated( options );
992
+ this.setRelated( related );
993
+
994
+ if ( !options.silent ) {
995
+ var dit = this;
996
+ Backbone.Relational.eventQueue.add( function() {
997
+ // The `changed` flag can be set in `handleAddition` or `handleRemoval`
998
+ if ( dit.changed ) {
999
+ dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
1000
+ dit.changed = false;
1001
+ }
1002
+ });
1003
+ }
1004
+ },
1005
+
1006
+ /**
1007
+ * When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations.
1008
+ * (should be 'HasOne', must set 'this.instance' as their related).
1009
+ */
1010
+ handleAddition: function( model, coll, options ) {
1011
+ //console.debug('handleAddition called; args=%o', arguments);
1012
+ options = options ? _.clone( options ) : {};
1013
+ this.changed = true;
1014
+
1015
+ _.each( this.getReverseRelations( model ), function( relation ) {
1016
+ relation.addRelated( this.instance, options );
1017
+ }, this );
1018
+
1019
+ // Only trigger 'add' once the newly added model is initialized (so, has its relations set up)
1020
+ var dit = this;
1021
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
1022
+ dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
1023
+ });
1024
+ },
1025
+
1026
+ /**
1027
+ * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations.
1028
+ * (should be 'HasOne', which should be nullified)
1029
+ */
1030
+ handleRemoval: function( model, coll, options ) {
1031
+ //console.debug('handleRemoval called; args=%o', arguments);
1032
+ options = options ? _.clone( options ) : {};
1033
+ this.changed = true;
1034
+
1035
+ _.each( this.getReverseRelations( model ), function( relation ) {
1036
+ relation.removeRelated( this.instance, null, options );
1037
+ }, this );
1038
+
1039
+ var dit = this;
1040
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
1041
+ dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
1042
+ });
1043
+ },
1044
+
1045
+ handleReset: function( coll, options ) {
1046
+ var dit = this;
1047
+ options = options ? _.clone( options ) : {};
1048
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
1049
+ dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
1050
+ });
1051
+ },
1052
+
1053
+ tryAddRelated: function( model, coll, options ) {
1054
+ var item = _.contains( this.keyIds, model.id );
1055
+
1056
+ if ( item ) {
1057
+ this.addRelated( model, options );
1058
+ this.keyIds = _.without( this.keyIds, model.id );
1059
+ }
1060
+ },
1061
+
1062
+ addRelated: function( model, options ) {
1063
+ // Allow 'model' to set up its relations before proceeding.
1064
+ // (which can result in a call to 'addRelated' from a relation of 'model')
1065
+ var dit = this;
1066
+ model.queue( function() {
1067
+ if ( dit.related && !dit.related.get( model ) ) {
1068
+ dit.related.add( model, _.defaults( { parse: false }, options ) );
1069
+ }
1070
+ });
1071
+ },
1072
+
1073
+ removeRelated: function( model, coll, options ) {
1074
+ if ( this.related.get( model ) ) {
1075
+ this.related.remove( model, options );
1076
+ }
1077
+ }
1078
+ });
1079
+
1080
+ /**
1081
+ * A type of Backbone.Model that also maintains relations to other models and collections.
1082
+ * New events when compared to the original:
1083
+ * - 'add:<key>' (model, related collection, options)
1084
+ * - 'remove:<key>' (model, related collection, options)
1085
+ * - 'change:<key>' (model, related model or collection, options)
1086
+ */
1087
+ Backbone.RelationalModel = Backbone.Model.extend({
1088
+ relations: null, // Relation descriptions on the prototype
1089
+ _relations: null, // Relation instances
1090
+ _isInitialized: false,
1091
+ _deferProcessing: false,
1092
+ _queue: null,
1093
+ _attributeChangeFired: false, // Keeps track of `change` event firing under some conditions (like nested `set`s)
1094
+
1095
+ subModelTypeAttribute: 'type',
1096
+ subModelTypes: null,
1097
+
1098
+ constructor: function( attributes, options ) {
1099
+ // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
1100
+ // Defer 'processQueue', so that when 'Relation.createModels' is used we trigger 'HasMany'
1101
+ // collection events only after the model is really fully set up.
1102
+ // Example: event for "p.on( 'add:jobs' )" -> "p.get('jobs').add( { company: c.id, person: p.id } )".
1103
+ if ( options && options.collection ) {
1104
+ var dit = this,
1105
+ collection = this.collection = options.collection;
1106
+
1107
+ // Prevent `collection` from cascading down to nested models; they shouldn't go into this `if` clause.
1108
+ delete options.collection;
1109
+
1110
+ this._deferProcessing = true;
1111
+
1112
+ var processQueue = function( model ) {
1113
+ if ( model === dit ) {
1114
+ dit._deferProcessing = false;
1115
+ dit.processQueue();
1116
+ collection.off( 'relational:add', processQueue );
1117
+ }
1118
+ };
1119
+ collection.on( 'relational:add', processQueue );
1120
+
1121
+ // So we do process the queue eventually, regardless of whether this model actually gets added to 'options.collection'.
1122
+ _.defer( function() {
1123
+ processQueue( dit );
1124
+ });
1125
+ }
1126
+
1127
+ Backbone.Relational.store.processOrphanRelations();
1128
+
1129
+ this._queue = new Backbone.BlockingQueue();
1130
+ this._queue.block();
1131
+ Backbone.Relational.eventQueue.block();
1132
+
1133
+ try {
1134
+ Backbone.Model.apply( this, arguments );
1135
+ }
1136
+ finally {
1137
+ // Try to run the global queue holding external events
1138
+ Backbone.Relational.eventQueue.unblock();
1139
+ }
1140
+ },
1141
+
1142
+ /**
1143
+ * Override 'trigger' to queue 'change' and 'change:*' events
1144
+ */
1145
+ trigger: function( eventName ) {
1146
+ if ( eventName.length > 5 && eventName.indexOf( 'change' ) === 0 ) {
1147
+ var dit = this,
1148
+ args = arguments;
1149
+
1150
+ Backbone.Relational.eventQueue.add( function() {
1151
+ if ( !dit._isInitialized ) {
1152
+ return;
1153
+ }
1154
+
1155
+ // Determine if the `change` event is still valid, now that all relations are populated
1156
+ var changed = true;
1157
+ if ( eventName === 'change' ) {
1158
+ // `hasChanged` may have gotten reset by nested calls to `set`.
1159
+ changed = dit.hasChanged() || dit._attributeChangeFired;
1160
+ dit._attributeChangeFired = false;
1161
+ }
1162
+ else {
1163
+ var attr = eventName.slice( 7 ),
1164
+ rel = dit.getRelation( attr );
1165
+
1166
+ if ( rel ) {
1167
+ // If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`.
1168
+ // These take precedence over `change:attr` events triggered by `Model.set`.
1169
+ // The relation sets a fourth attribute to `true`. If this attribute is present,
1170
+ // continue triggering this event; otherwise, it's from `Model.set` and should be stopped.
1171
+ changed = ( args[ 4 ] === true );
1172
+
1173
+ // If this event was triggered by a relation, set the right value in `this.changed`
1174
+ // (a Collection or Model instead of raw data).
1175
+ if ( changed ) {
1176
+ dit.changed[ attr ] = args[ 2 ];
1177
+ }
1178
+ // Otherwise, this event is from `Model.set`. If the relation doesn't report a change,
1179
+ // remove attr from `dit.changed` so `hasChanged` doesn't take it into account.
1180
+ else if ( !rel.changed ) {
1181
+ delete dit.changed[ attr ];
1182
+ }
1183
+ }
1184
+ else if ( changed ) {
1185
+ dit._attributeChangeFired = true;
1186
+ }
1187
+ }
1188
+
1189
+ changed && Backbone.Model.prototype.trigger.apply( dit, args );
1190
+ });
1191
+ }
1192
+ else {
1193
+ Backbone.Model.prototype.trigger.apply( this, arguments );
1194
+ }
1195
+
1196
+ return this;
1197
+ },
1198
+
1199
+ /**
1200
+ * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance.
1201
+ * Invoked in the first call so 'set' (which is made from the Backbone.Model constructor).
1202
+ */
1203
+ initializeRelations: function( options ) {
1204
+ this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once
1205
+ this._relations = {};
1206
+
1207
+ _.each( _.result( this, 'relations' ) || [], function( rel ) {
1208
+ Backbone.Relational.store.initializeRelation( this, rel, options );
1209
+ }, this );
1210
+
1211
+ this._isInitialized = true;
1212
+ this.release();
1213
+ this.processQueue();
1214
+ },
1215
+
1216
+ /**
1217
+ * When new values are set, notify this model's relations (also if options.silent is set).
1218
+ * (called from `set`; Relation.setRelated locks this model before calling 'set' on it to prevent loops)
1219
+ * @param {Object} [changedAttrs]
1220
+ * @param {Object} [options]
1221
+ */
1222
+ updateRelations: function( changedAttrs, options ) {
1223
+ if ( this._isInitialized && !this.isLocked() ) {
1224
+ _.each( this._relations, function( rel ) {
1225
+ if ( !changedAttrs || ( rel.keySource in changedAttrs || rel.key in changedAttrs ) ) {
1226
+ // Fetch data in `rel.keySource` if data got set in there, or `rel.key` otherwise
1227
+ var value = this.attributes[ rel.keySource ] || this.attributes[ rel.key ],
1228
+ attr = changedAttrs && ( changedAttrs[ rel.keySource ] || changedAttrs[ rel.key ] );
1229
+
1230
+ // Update a relation if its value differs from this model's attributes, or it's been explicitly nullified.
1231
+ // Which can also happen before the originally intended related model has been found (`val` is null).
1232
+ if ( rel.related !== value || ( value === null && attr === null ) ) {
1233
+ this.trigger( 'relational:change:' + rel.key, this, value, options || {} );
1234
+ }
1235
+ }
1236
+
1237
+ // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
1238
+ if ( rel.keySource !== rel.key ) {
1239
+ delete this.attributes[ rel.keySource ];
1240
+ }
1241
+ }, this );
1242
+ }
1243
+ },
1244
+
1245
+ /**
1246
+ * Either add to the queue (if we're not initialized yet), or execute right away.
1247
+ */
1248
+ queue: function( func ) {
1249
+ this._queue.add( func );
1250
+ },
1251
+
1252
+ /**
1253
+ * Process _queue
1254
+ */
1255
+ processQueue: function() {
1256
+ if ( this._isInitialized && !this._deferProcessing && this._queue.isBlocked() ) {
1257
+ this._queue.unblock();
1258
+ }
1259
+ },
1260
+
1261
+ /**
1262
+ * Get a specific relation.
1263
+ * @param {string} key The relation key to look for.
1264
+ * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'key', or null.
1265
+ */
1266
+ getRelation: function( key ) {
1267
+ return this._relations[ key ];
1268
+ },
1269
+
1270
+ /**
1271
+ * Get all of the created relations.
1272
+ * @return {Backbone.Relation[]}
1273
+ */
1274
+ getRelations: function() {
1275
+ return _.values( this._relations );
1276
+ },
1277
+
1278
+ /**
1279
+ * Retrieve related objects.
1280
+ * @param {string} key The relation key to fetch models for.
1281
+ * @param {Object} [options] Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
1282
+ * @param {Boolean} [refresh=false] Fetch existing models from the server as well (in order to update them).
1283
+ * @return {jQuery.when[]} An array of request objects
1284
+ */
1285
+ fetchRelated: function( key, options, refresh ) {
1286
+ // Set default `options` for fetch
1287
+ options = _.extend( { update: true, remove: false }, options );
1288
+
1289
+ var models,
1290
+ setUrl,
1291
+ requests = [],
1292
+ rel = this.getRelation( key ),
1293
+ idsToFetch = rel && ( ( rel.keyIds && rel.keyIds.slice( 0 ) ) || ( ( rel.keyId || rel.keyId === 0 ) ? [ rel.keyId ] : [] ) );
1294
+
1295
+ // On `refresh`, add the ids for current models in the relation to `idsToFetch`
1296
+ if ( refresh ) {
1297
+ models = rel.related instanceof Backbone.Collection ? rel.related.models : [ rel.related ];
1298
+ _.each( models, function( model ) {
1299
+ if ( model.id || model.id === 0 ) {
1300
+ idsToFetch.push( model.id );
1301
+ }
1302
+ });
1303
+ }
1304
+
1305
+ if ( idsToFetch && idsToFetch.length ) {
1306
+ // Find (or create) a model for each one that is to be fetched
1307
+ var created = [];
1308
+ models = _.map( idsToFetch, function( id ) {
1309
+ var model = Backbone.Relational.store.find( rel.relatedModel, id );
1310
+
1311
+ if ( !model ) {
1312
+ var attrs = {};
1313
+ attrs[ rel.relatedModel.prototype.idAttribute ] = id;
1314
+ model = rel.relatedModel.findOrCreate( attrs, options );
1315
+ created.push( model );
1316
+ }
1317
+
1318
+ return model;
1319
+ }, this );
1320
+
1321
+ // Try if the 'collection' can provide a url to fetch a set of models in one request.
1322
+ if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) {
1323
+ setUrl = rel.related.url( models );
1324
+ }
1325
+
1326
+ // An assumption is that when 'Backbone.Collection.url' is a function, it can handle building of set urls.
1327
+ // To make sure it can, test if the url we got by supplying a list of models to fetch is different from
1328
+ // the one supplied for the default fetch action (without args to 'url').
1329
+ if ( setUrl && setUrl !== rel.related.url() ) {
1330
+ var opts = _.defaults(
1331
+ {
1332
+ error: function() {
1333
+ var args = arguments;
1334
+ _.each( created, function( model ) {
1335
+ model.trigger( 'destroy', model, model.collection, options );
1336
+ options.error && options.error.apply( model, args );
1337
+ });
1338
+ },
1339
+ url: setUrl
1340
+ },
1341
+ options
1342
+ );
1343
+
1344
+ requests = [ rel.related.fetch( opts ) ];
1345
+ }
1346
+ else {
1347
+ requests = _.map( models, function( model ) {
1348
+ var opts = _.defaults(
1349
+ {
1350
+ error: function() {
1351
+ if ( _.contains( created, model ) ) {
1352
+ model.trigger( 'destroy', model, model.collection, options );
1353
+ options.error && options.error.apply( model, arguments );
1354
+ }
1355
+ }
1356
+ },
1357
+ options
1358
+ );
1359
+ return model.fetch( opts );
1360
+ }, this );
1361
+ }
1362
+ }
1363
+
1364
+ return requests;
1365
+ },
1366
+
1367
+ get: function( attr ) {
1368
+ var originalResult = Backbone.Model.prototype.get.call( this, attr );
1369
+
1370
+ // Use `originalResult` get if dotNotation not enabled or not required because no dot is in `attr`
1371
+ if ( !this.dotNotation || attr.indexOf( '.' ) === -1 ) {
1372
+ return originalResult;
1373
+ }
1374
+
1375
+ // Go through all splits and return the final result
1376
+ var splits = attr.split( '.' );
1377
+ var result = _.reduce(splits, function( model, split ) {
1378
+ if ( _.isNull(model) || _.isUndefined( model ) ) {
1379
+ // Return undefined if the path cannot be expanded
1380
+ return undefined;
1381
+ }
1382
+ else if ( model instanceof Backbone.Model ) {
1383
+ return Backbone.Model.prototype.get.call( model, split );
1384
+ }
1385
+ else if ( model instanceof Backbone.Collection ) {
1386
+ return Backbone.Collection.prototype.at.call( model, split )
1387
+ }
1388
+ else {
1389
+ throw new Error( 'Attribute must be an instanceof Backbone.Model or Backbone.Collection. Is: ' + model + ', currentSplit: ' + split );
1390
+ }
1391
+ }, this );
1392
+
1393
+ if ( originalResult !== undefined && result !== undefined ) {
1394
+ throw new Error( "Ambiguous result for '" + attr + "'. direct result: " + originalResult + ", dotNotation: " + result );
1395
+ }
1396
+
1397
+ return originalResult || result;
1398
+ },
1399
+
1400
+ set: function( key, value, options ) {
1401
+ Backbone.Relational.eventQueue.block();
1402
+
1403
+ // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object
1404
+ var attributes;
1405
+ if ( _.isObject( key ) || key == null ) {
1406
+ attributes = key;
1407
+ options = value;
1408
+ }
1409
+ else {
1410
+ attributes = {};
1411
+ attributes[ key ] = value;
1412
+ }
1413
+
1414
+ try {
1415
+ var id = this.id,
1416
+ newId = attributes && this.idAttribute in attributes && attributes[ this.idAttribute ];
1417
+
1418
+ // Check if we're not setting a duplicate id before actually calling `set`.
1419
+ Backbone.Relational.store.checkId( this, newId );
1420
+
1421
+ var result = Backbone.Model.prototype.set.apply( this, arguments );
1422
+
1423
+ // Ideal place to set up relations, if this is the first time we're here for this model
1424
+ if ( !this._isInitialized && !this.isLocked() ) {
1425
+ this.constructor.initializeModelHierarchy();
1426
+ Backbone.Relational.store.register( this );
1427
+ this.initializeRelations( options );
1428
+ }
1429
+ // The store should know about an `id` update asap
1430
+ else if ( newId && newId !== id ) {
1431
+ Backbone.Relational.store.update( this );
1432
+ }
1433
+
1434
+ if ( attributes ) {
1435
+ this.updateRelations( attributes, options );
1436
+ }
1437
+ }
1438
+ finally {
1439
+ // Try to run the global queue holding external events
1440
+ Backbone.Relational.eventQueue.unblock();
1441
+ }
1442
+
1443
+ return result;
1444
+ },
1445
+
1446
+ clone: function() {
1447
+ var attributes = _.clone( this.attributes );
1448
+ if ( !_.isUndefined( attributes[ this.idAttribute ] ) ) {
1449
+ attributes[ this.idAttribute ] = null;
1450
+ }
1451
+
1452
+ _.each( this.getRelations(), function( rel ) {
1453
+ delete attributes[ rel.key ];
1454
+ });
1455
+
1456
+ return new this.constructor( attributes );
1457
+ },
1458
+
1459
+ /**
1460
+ * Convert relations to JSON, omits them when required
1461
+ */
1462
+ toJSON: function( options ) {
1463
+ // If this Model has already been fully serialized in this branch once, return to avoid loops
1464
+ if ( this.isLocked() ) {
1465
+ return this.id;
1466
+ }
1467
+
1468
+ this.acquire();
1469
+ var json = Backbone.Model.prototype.toJSON.call( this, options );
1470
+
1471
+ if ( this.constructor._superModel && !( this.constructor._subModelTypeAttribute in json ) ) {
1472
+ json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue;
1473
+ }
1474
+
1475
+ _.each( this._relations, function( rel ) {
1476
+ var related = json[ rel.key ],
1477
+ includeInJSON = rel.options.includeInJSON,
1478
+ value = null;
1479
+
1480
+ if ( includeInJSON === true ) {
1481
+ if ( related && _.isFunction( related.toJSON ) ) {
1482
+ value = related.toJSON( options );
1483
+ }
1484
+ }
1485
+ else if ( _.isString( includeInJSON ) ) {
1486
+ if ( related instanceof Backbone.Collection ) {
1487
+ value = related.pluck( includeInJSON );
1488
+ }
1489
+ else if ( related instanceof Backbone.Model ) {
1490
+ value = related.get( includeInJSON );
1491
+ }
1492
+
1493
+ // Add ids for 'unfound' models if includeInJSON is equal to (only) the relatedModel's `idAttribute`
1494
+ if ( includeInJSON === rel.relatedModel.prototype.idAttribute ) {
1495
+ if ( rel instanceof Backbone.HasMany ) {
1496
+ value = value.concat( rel.keyIds );
1497
+ }
1498
+ else if ( rel instanceof Backbone.HasOne ) {
1499
+ value = value || rel.keyId;
1500
+
1501
+ if ( !value && !_.isObject( rel.keyContents ) ) {
1502
+ value = rel.keyContents || null;
1503
+ }
1504
+ }
1505
+ }
1506
+ }
1507
+ else if ( _.isArray( includeInJSON ) ) {
1508
+ if ( related instanceof Backbone.Collection ) {
1509
+ value = [];
1510
+ related.each( function( model ) {
1511
+ var curJson = {};
1512
+ _.each( includeInJSON, function( key ) {
1513
+ curJson[ key ] = model.get( key );
1514
+ });
1515
+ value.push( curJson );
1516
+ });
1517
+ }
1518
+ else if ( related instanceof Backbone.Model ) {
1519
+ value = {};
1520
+ _.each( includeInJSON, function( key ) {
1521
+ value[ key ] = related.get( key );
1522
+ });
1523
+ }
1524
+ }
1525
+ else {
1526
+ delete json[ rel.key ];
1527
+ }
1528
+
1529
+ if ( includeInJSON ) {
1530
+ json[ rel.keyDestination ] = value;
1531
+ }
1532
+
1533
+ if ( rel.keyDestination !== rel.key ) {
1534
+ delete json[ rel.key ];
1535
+ }
1536
+ });
1537
+
1538
+ this.release();
1539
+ return json;
1540
+ }
1541
+ },
1542
+ {
1543
+ /**
1544
+ *
1545
+ * @param superModel
1546
+ * @returns {Backbone.RelationalModel.constructor}
1547
+ */
1548
+ setup: function( superModel ) {
1549
+ // We don't want to share a relations array with a parent, as this will cause problems with
1550
+ // reverse relations.
1551
+ this.prototype.relations = ( this.prototype.relations || [] ).slice( 0 );
1552
+
1553
+ this._subModels = {};
1554
+ this._superModel = null;
1555
+
1556
+ // If this model has 'subModelTypes' itself, remember them in the store
1557
+ if ( this.prototype.hasOwnProperty( 'subModelTypes' ) ) {
1558
+ Backbone.Relational.store.addSubModels( this.prototype.subModelTypes, this );
1559
+ }
1560
+ // The 'subModelTypes' property should not be inherited, so reset it.
1561
+ else {
1562
+ this.prototype.subModelTypes = null;
1563
+ }
1564
+
1565
+ // Initialize all reverseRelations that belong to this new model.
1566
+ _.each( this.prototype.relations || [], function( rel ) {
1567
+ if ( !rel.model ) {
1568
+ rel.model = this;
1569
+ }
1570
+
1571
+ if ( rel.reverseRelation && rel.model === this ) {
1572
+ var preInitialize = true;
1573
+ if ( _.isString( rel.relatedModel ) ) {
1574
+ /**
1575
+ * The related model might not be defined for two reasons
1576
+ * 1. it is related to itself
1577
+ * 2. it never gets defined, e.g. a typo
1578
+ * 3. the model hasn't been defined yet, but will be later
1579
+ * In neither of these cases do we need to pre-initialize reverse relations.
1580
+ * However, for 3. (which is, to us, indistinguishable from 2.), we do need to attempt
1581
+ * setting up this relation again later, in case the related model is defined later.
1582
+ */
1583
+ var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
1584
+ preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
1585
+ }
1586
+
1587
+ if ( preInitialize ) {
1588
+ Backbone.Relational.store.initializeRelation( null, rel );
1589
+ }
1590
+ else if ( _.isString( rel.relatedModel ) ) {
1591
+ Backbone.Relational.store.addOrphanRelation( rel );
1592
+ }
1593
+ }
1594
+ }, this );
1595
+
1596
+ return this;
1597
+ },
1598
+
1599
+ /**
1600
+ * Create a 'Backbone.Model' instance based on 'attributes'.
1601
+ * @param {Object} attributes
1602
+ * @param {Object} [options]
1603
+ * @return {Backbone.Model}
1604
+ */
1605
+ build: function( attributes, options ) {
1606
+ // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet.
1607
+ this.initializeModelHierarchy();
1608
+
1609
+ // Determine what type of (sub)model should be built if applicable.
1610
+ var model = this._findSubModelType( this, attributes ) || this;
1611
+
1612
+ return new model( attributes, options );
1613
+ },
1614
+
1615
+ /**
1616
+ * Determines what type of (sub)model should be built if applicable.
1617
+ * Looks up the proper subModelType in 'this._subModels', recursing into
1618
+ * types until a match is found. Returns the applicable 'Backbone.Model'
1619
+ * or null if no match is found.
1620
+ * @param {Backbone.Model} type
1621
+ * @param {Object} attributes
1622
+ * @return {Backbone.Model}
1623
+ */
1624
+ _findSubModelType: function( type, attributes ) {
1625
+ if ( type._subModels && type.prototype.subModelTypeAttribute in attributes ) {
1626
+ var subModelTypeAttribute = attributes[ type.prototype.subModelTypeAttribute ];
1627
+ var subModelType = type._subModels[ subModelTypeAttribute ];
1628
+ if ( subModelType ) {
1629
+ return subModelType;
1630
+ }
1631
+ else {
1632
+ // Recurse into subModelTypes to find a match
1633
+ for ( subModelTypeAttribute in type._subModels ) {
1634
+ subModelType = this._findSubModelType( type._subModels[ subModelTypeAttribute ], attributes );
1635
+ if ( subModelType ) {
1636
+ return subModelType;
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1641
+ return null;
1642
+ },
1643
+
1644
+ /**
1645
+ *
1646
+ */
1647
+ initializeModelHierarchy: function() {
1648
+ // Inherit any relations that have been defined in the parent model.
1649
+ this.inheritRelations();
1650
+
1651
+ // If we came here through 'build' for a model that has 'subModelTypes' then try to initialize the ones that
1652
+ // haven't been resolved yet.
1653
+ if ( this.prototype.subModelTypes ) {
1654
+ var resolvedSubModels = _.keys( this._subModels );
1655
+ var unresolvedSubModels = _.omit( this.prototype.subModelTypes, resolvedSubModels );
1656
+ _.each( unresolvedSubModels, function( subModelTypeName ) {
1657
+ var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
1658
+ subModelType && subModelType.initializeModelHierarchy();
1659
+ });
1660
+ }
1661
+ },
1662
+
1663
+ inheritRelations: function() {
1664
+ // Bail out if we've been here before.
1665
+ if (!_.isUndefined( this._superModel ) && !_.isNull( this._superModel )) {
1666
+ return;
1667
+ }
1668
+ // Try to initialize the _superModel.
1669
+ Backbone.Relational.store.setupSuperModel( this );
1670
+
1671
+ // If a superModel has been found, copy relations from the _superModel if they haven't been inherited automatically
1672
+ // (due to a redefinition of 'relations').
1673
+ if ( this._superModel ) {
1674
+ // The _superModel needs a chance to initialize its own inherited relations before we attempt to inherit relations
1675
+ // from the _superModel. You don't want to call 'initializeModelHierarchy' because that could cause sub-models of
1676
+ // this class to inherit their relations before this class has had chance to inherit it's relations.
1677
+ this._superModel.inheritRelations();
1678
+ if ( this._superModel.prototype.relations ) {
1679
+ // Find relations that exist on the '_superModel', but not yet on this model.
1680
+ var inheritedRelations = _.select( this._superModel.prototype.relations || [], function( superRel ) {
1681
+ return !_.any( this.prototype.relations || [], function( rel ) {
1682
+ return superRel.relatedModel === rel.relatedModel && superRel.key === rel.key;
1683
+ }, this );
1684
+ }, this );
1685
+
1686
+ this.prototype.relations = inheritedRelations.concat( this.prototype.relations );
1687
+ }
1688
+ }
1689
+ // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail the
1690
+ // isUndefined/isNull check next time.
1691
+ else {
1692
+ this._superModel = false;
1693
+ }
1694
+ },
1695
+
1696
+ /**
1697
+ * Find an instance of `this` type in 'Backbone.Relational.store'.
1698
+ * - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found.
1699
+ * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
1700
+ * Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`).
1701
+ * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
1702
+ * @param {Object} [options]
1703
+ * @param {Boolean} [options.create=true]
1704
+ * @param {Boolean} [options.merge=true]
1705
+ * @param {Boolean} [options.parse=false]
1706
+ * @return {Backbone.RelationalModel}
1707
+ */
1708
+ findOrCreate: function( attributes, options ) {
1709
+ options || ( options = {} );
1710
+ var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ?
1711
+ this.prototype.parse( _.clone( attributes ) ) : attributes;
1712
+
1713
+ // Try to find an instance of 'this' model type in the store
1714
+ var model = Backbone.Relational.store.find( this, parsedAttributes );
1715
+
1716
+ // If we found an instance, update it with the data in 'item' (unless 'options.merge' is false).
1717
+ // If not, create an instance (unless 'options.create' is false).
1718
+ if ( _.isObject( attributes ) ) {
1719
+ if ( model && options.merge !== false ) {
1720
+ // Make sure `options.collection` and `options.url` doesn't cascade to nested models
1721
+ delete options.collection;
1722
+ delete options.url;
1723
+
1724
+ model.set( parsedAttributes, options );
1725
+ }
1726
+ else if ( !model && options.create !== false ) {
1727
+ model = this.build( attributes, options );
1728
+ }
1729
+ }
1730
+
1731
+ return model;
1732
+ },
1733
+
1734
+ /**
1735
+ * Find an instance of `this` type in 'Backbone.Relational.store'.
1736
+ * - If `attributes` is a string or a number, `find` will just query the `store` and return a model if found.
1737
+ * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
1738
+ * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
1739
+ * @param {Object} [options]
1740
+ * @param {Boolean} [options.merge=true]
1741
+ * @param {Boolean} [options.parse=false]
1742
+ * @return {Backbone.RelationalModel}
1743
+ */
1744
+ find: function( attributes, options ) {
1745
+ options || ( options = {} );
1746
+ options.create = false;
1747
+ return this.findOrCreate( attributes, options );
1748
+ }
1749
+ });
1750
+ _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
1751
+
1752
+ /**
1753
+ * Override Backbone.Collection._prepareModel, so objects will be built using the correct type
1754
+ * if the collection.model has subModels.
1755
+ * Attempts to find a model for `attrs` in Backbone.store through `findOrCreate`
1756
+ * (which sets the new properties on it if found), or instantiates a new model.
1757
+ */
1758
+ Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel;
1759
+ Backbone.Collection.prototype._prepareModel = function ( attrs, options ) {
1760
+ var model;
1761
+
1762
+ if ( attrs instanceof Backbone.Model ) {
1763
+ if ( !attrs.collection ) {
1764
+ attrs.collection = this;
1765
+ }
1766
+ model = attrs;
1767
+ }
1768
+ else {
1769
+ options = options ? _.clone( options ) : {};
1770
+ options.collection = this;
1771
+
1772
+ if ( typeof this.model.findOrCreate !== 'undefined' ) {
1773
+ model = this.model.findOrCreate( attrs, options );
1774
+ }
1775
+ else {
1776
+ model = new this.model( attrs, options );
1777
+ }
1778
+
1779
+ if ( model && model.validationError ) {
1780
+ this.trigger( 'invalid', this, attrs, options );
1781
+ model = false;
1782
+ }
1783
+ }
1784
+
1785
+ return model;
1786
+ };
1787
+
1788
+
1789
+ /**
1790
+ * Override Backbone.Collection.set, so we'll create objects from attributes where required,
1791
+ * and update the existing models. Also, trigger 'relational:add'.
1792
+ */
1793
+ var set = Backbone.Collection.prototype.__set = Backbone.Collection.prototype.set;
1794
+ Backbone.Collection.prototype.set = function( models, options ) {
1795
+ // Short-circuit if this Collection doesn't hold RelationalModels
1796
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
1797
+ return set.apply( this, arguments );
1798
+ }
1799
+
1800
+ if ( options && options.parse ) {
1801
+ models = this.parse( models, options );
1802
+ }
1803
+
1804
+ models = _.isArray( models ) ? models.slice() : ( models ? [ models ] : [] );
1805
+
1806
+ var newModels = [],
1807
+ toAdd = [];
1808
+
1809
+ //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
1810
+ _.each( models, function( model ) {
1811
+ if ( !( model instanceof Backbone.Model ) ) {
1812
+ model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
1813
+ }
1814
+
1815
+ if ( model ) {
1816
+ toAdd.push( model );
1817
+
1818
+ if ( !( this.get( model ) || this.get( model.cid ) ) ) {
1819
+ newModels.push( model );
1820
+ }
1821
+ // If we arrive in `add` while performing a `set` (after a create, so the model gains an `id`),
1822
+ // we may get here before `_onModelEvent` has had the chance to update `_byId`.
1823
+ else if ( model.id != null ) {
1824
+ this._byId[ model.id ] = model;
1825
+ }
1826
+ }
1827
+ }, this );
1828
+
1829
+ // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc).
1830
+ // If `parse` was specified, the collection and contained models have been parsed now.
1831
+ var result = set.call( this, toAdd, _.defaults( { parse: false }, options ) );
1832
+
1833
+ _.each( newModels, function( model ) {
1834
+ // Fire a `relational:add` event for any model in `newModels` that has actually been added to the collection.
1835
+ if ( this.get( model ) || this.get( model.cid ) ) {
1836
+ this.trigger( 'relational:add', model, this, options );
1837
+ }
1838
+ }, this );
1839
+
1840
+ return result;
1841
+ };
1842
+
1843
+ /**
1844
+ * Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
1845
+ */
1846
+ var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove;
1847
+ Backbone.Collection.prototype.remove = function( models, options ) {
1848
+ // Short-circuit if this Collection doesn't hold RelationalModels
1849
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
1850
+ return remove.apply( this, arguments );
1851
+ }
1852
+
1853
+ models = _.isArray( models ) ? models.slice() : [ models ];
1854
+ options || ( options = {} );
1855
+
1856
+ var toRemove = [];
1857
+
1858
+ //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
1859
+ _.each( models, function( model ) {
1860
+ model = this.get( model ) || ( model && this.get( model.cid ) );
1861
+ model && toRemove.push( model );
1862
+ }, this );
1863
+
1864
+ var result = remove.call( this, toRemove, options );
1865
+
1866
+ _.each( toRemove, function( model ) {
1867
+ this.trigger('relational:remove', model, this, options);
1868
+ }, this );
1869
+
1870
+ return result;
1871
+ };
1872
+
1873
+ /**
1874
+ * Override 'Backbone.Collection.reset' to trigger 'relational:reset'.
1875
+ */
1876
+ var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset;
1877
+ Backbone.Collection.prototype.reset = function( models, options ) {
1878
+ options = _.extend( { merge: true }, options );
1879
+ var result = reset.call( this, models, options );
1880
+
1881
+ if ( this.model.prototype instanceof Backbone.RelationalModel ) {
1882
+ this.trigger( 'relational:reset', this, options );
1883
+ }
1884
+
1885
+ return result;
1886
+ };
1887
+
1888
+ /**
1889
+ * Override 'Backbone.Collection.sort' to trigger 'relational:reset'.
1890
+ */
1891
+ var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort;
1892
+ Backbone.Collection.prototype.sort = function( options ) {
1893
+ var result = sort.call( this, options );
1894
+
1895
+ if ( this.model.prototype instanceof Backbone.RelationalModel ) {
1896
+ this.trigger( 'relational:reset', this, options );
1897
+ }
1898
+
1899
+ return result;
1900
+ };
1901
+
1902
+ /**
1903
+ * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations
1904
+ * are ready.
1905
+ */
1906
+ var trigger = Backbone.Collection.prototype.__trigger = Backbone.Collection.prototype.trigger;
1907
+ Backbone.Collection.prototype.trigger = function( eventName ) {
1908
+ // Short-circuit if this Collection doesn't hold RelationalModels
1909
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
1910
+ return trigger.apply( this, arguments );
1911
+ }
1912
+
1913
+ if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' || eventName === 'sort' ) {
1914
+ var dit = this,
1915
+ args = arguments;
1916
+
1917
+ if ( _.isObject( args[ 3 ] ) ) {
1918
+ args = _.toArray( args );
1919
+ // the fourth argument is the option object.
1920
+ // we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked
1921
+ args[ 3 ] = _.clone( args[ 3 ] );
1922
+ }
1923
+
1924
+ Backbone.Relational.eventQueue.add( function() {
1925
+ trigger.apply( dit, args );
1926
+ });
1927
+ }
1928
+ else {
1929
+ trigger.apply( this, arguments );
1930
+ }
1931
+
1932
+ return this;
1933
+ };
1934
+
1935
+ // Override .extend() to automatically call .setup()
1936
+ Backbone.RelationalModel.extend = function( protoProps, classProps ) {
1937
+ var child = Backbone.Model.extend.apply( this, arguments );
1938
+
1939
+ child.setup( this );
1940
+
1941
+ return child;
1942
+ };
1943
+ })();