1 | /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */ |
---|
2 | /** |
---|
3 | * Backbone-relational.js 0.7.1 |
---|
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 = module.exports = Backbone; |
---|
21 | } |
---|
22 | else { |
---|
23 | _ = window._; |
---|
24 | Backbone = window.Backbone; |
---|
25 | exports = window; |
---|
26 | } |
---|
27 | |
---|
28 | Backbone.Relational = { |
---|
29 | showWarnings: true |
---|
30 | }; |
---|
31 | |
---|
32 | /** |
---|
33 | * Semaphore mixin; can be used as both binary and counting. |
---|
34 | **/ |
---|
35 | Backbone.Semaphore = { |
---|
36 | _permitsAvailable: null, |
---|
37 | _permitsUsed: 0, |
---|
38 | |
---|
39 | acquire: function() { |
---|
40 | if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) { |
---|
41 | throw new Error( 'Max permits acquired' ); |
---|
42 | } |
---|
43 | else { |
---|
44 | this._permitsUsed++; |
---|
45 | } |
---|
46 | }, |
---|
47 | |
---|
48 | release: function() { |
---|
49 | if ( this._permitsUsed === 0 ) { |
---|
50 | throw new Error( 'All permits released' ); |
---|
51 | } |
---|
52 | else { |
---|
53 | this._permitsUsed--; |
---|
54 | } |
---|
55 | }, |
---|
56 | |
---|
57 | isLocked: function() { |
---|
58 | return this._permitsUsed > 0; |
---|
59 | }, |
---|
60 | |
---|
61 | setAvailablePermits: function( amount ) { |
---|
62 | if ( this._permitsUsed > amount ) { |
---|
63 | throw new Error( 'Available permits cannot be less than used permits' ); |
---|
64 | } |
---|
65 | this._permitsAvailable = amount; |
---|
66 | } |
---|
67 | }; |
---|
68 | |
---|
69 | /** |
---|
70 | * A BlockingQueue that accumulates items while blocked (via 'block'), |
---|
71 | * and processes them when unblocked (via 'unblock'). |
---|
72 | * Process can also be called manually (via 'process'). |
---|
73 | */ |
---|
74 | Backbone.BlockingQueue = function() { |
---|
75 | this._queue = []; |
---|
76 | }; |
---|
77 | _.extend( Backbone.BlockingQueue.prototype, Backbone.Semaphore, { |
---|
78 | _queue: null, |
---|
79 | |
---|
80 | add: function( func ) { |
---|
81 | if ( this.isBlocked() ) { |
---|
82 | this._queue.push( func ); |
---|
83 | } |
---|
84 | else { |
---|
85 | func(); |
---|
86 | } |
---|
87 | }, |
---|
88 | |
---|
89 | process: function() { |
---|
90 | while ( this._queue && this._queue.length ) { |
---|
91 | this._queue.shift()(); |
---|
92 | } |
---|
93 | }, |
---|
94 | |
---|
95 | block: function() { |
---|
96 | this.acquire(); |
---|
97 | }, |
---|
98 | |
---|
99 | unblock: function() { |
---|
100 | this.release(); |
---|
101 | if ( !this.isBlocked() ) { |
---|
102 | this.process(); |
---|
103 | } |
---|
104 | }, |
---|
105 | |
---|
106 | isBlocked: function() { |
---|
107 | return this.isLocked(); |
---|
108 | } |
---|
109 | }); |
---|
110 | /** |
---|
111 | * Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'update:<key>') |
---|
112 | * until the top-level object is fully initialized (see 'Backbone.RelationalModel'). |
---|
113 | */ |
---|
114 | Backbone.Relational.eventQueue = new Backbone.BlockingQueue(); |
---|
115 | |
---|
116 | /** |
---|
117 | * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel. |
---|
118 | * Handles lookup for relations. |
---|
119 | */ |
---|
120 | Backbone.Store = function() { |
---|
121 | this._collections = []; |
---|
122 | this._reverseRelations = []; |
---|
123 | this._subModels = []; |
---|
124 | this._modelScopes = [ exports ]; |
---|
125 | }; |
---|
126 | _.extend( Backbone.Store.prototype, Backbone.Events, { |
---|
127 | addModelScope: function( scope ) { |
---|
128 | this._modelScopes.push( scope ); |
---|
129 | }, |
---|
130 | |
---|
131 | /** |
---|
132 | * Add a set of subModelTypes to the store, that can be used to resolve the '_superModel' |
---|
133 | * for a model later in 'setupSuperModel'. |
---|
134 | * |
---|
135 | * @param {Backbone.RelationalModel} subModelTypes |
---|
136 | * @param {Backbone.RelationalModel} superModelType |
---|
137 | */ |
---|
138 | addSubModels: function( subModelTypes, superModelType ) { |
---|
139 | this._subModels.push({ |
---|
140 | 'superModelType': superModelType, |
---|
141 | 'subModels': subModelTypes |
---|
142 | }); |
---|
143 | }, |
---|
144 | |
---|
145 | /** |
---|
146 | * Check if the given modelType is registered as another model's subModel. If so, add it to the super model's |
---|
147 | * '_subModels', and set the modelType's '_superModel', '_subModelTypeName', and '_subModelTypeAttribute'. |
---|
148 | * |
---|
149 | * @param {Backbone.RelationalModel} modelType |
---|
150 | */ |
---|
151 | setupSuperModel: function( modelType ) { |
---|
152 | _.find( this._subModels || [], function( subModelDef ) { |
---|
153 | return _.find( subModelDef.subModels || [], function( subModelTypeName, typeValue ) { |
---|
154 | var subModelType = this.getObjectByName( subModelTypeName ); |
---|
155 | |
---|
156 | if ( modelType === subModelType ) { |
---|
157 | // Set 'modelType' as a child of the found superModel |
---|
158 | subModelDef.superModelType._subModels[ typeValue ] = modelType; |
---|
159 | |
---|
160 | // Set '_superModel', '_subModelTypeValue', and '_subModelTypeAttribute' on 'modelType'. |
---|
161 | modelType._superModel = subModelDef.superModelType; |
---|
162 | modelType._subModelTypeValue = typeValue; |
---|
163 | modelType._subModelTypeAttribute = subModelDef.superModelType.prototype.subModelTypeAttribute; |
---|
164 | return true; |
---|
165 | } |
---|
166 | }, this ); |
---|
167 | }, this ); |
---|
168 | }, |
---|
169 | |
---|
170 | /** |
---|
171 | * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to |
---|
172 | * existing instances of 'model' in the store as well. |
---|
173 | * @param {Object} relation |
---|
174 | * @param {Backbone.RelationalModel} relation.model |
---|
175 | * @param {String} relation.type |
---|
176 | * @param {String} relation.key |
---|
177 | * @param {String|Object} relation.relatedModel |
---|
178 | */ |
---|
179 | addReverseRelation: function( relation ) { |
---|
180 | var exists = _.any( this._reverseRelations || [], function( rel ) { |
---|
181 | return _.all( relation || [], function( val, key ) { |
---|
182 | return val === rel[ key ]; |
---|
183 | }); |
---|
184 | }); |
---|
185 | |
---|
186 | if ( !exists && relation.model && relation.type ) { |
---|
187 | this._reverseRelations.push( relation ); |
---|
188 | |
---|
189 | var addRelation = function( model, relation ) { |
---|
190 | if ( !model.prototype.relations ) { |
---|
191 | model.prototype.relations = []; |
---|
192 | } |
---|
193 | model.prototype.relations.push( relation ); |
---|
194 | |
---|
195 | _.each( model._subModels || [], function( subModel ) { |
---|
196 | addRelation( subModel, relation ); |
---|
197 | }, this ); |
---|
198 | }; |
---|
199 | |
---|
200 | addRelation( relation.model, relation ); |
---|
201 | |
---|
202 | this.retroFitRelation( relation ); |
---|
203 | } |
---|
204 | }, |
---|
205 | |
---|
206 | /** |
---|
207 | * Add a 'relation' to all existing instances of 'relation.model' in the store |
---|
208 | * @param {Object} relation |
---|
209 | */ |
---|
210 | retroFitRelation: function( relation ) { |
---|
211 | var coll = this.getCollection( relation.model ); |
---|
212 | coll.each( function( model ) { |
---|
213 | if ( !( model instanceof relation.model ) ) { |
---|
214 | return; |
---|
215 | } |
---|
216 | |
---|
217 | new relation.type( model, relation ); |
---|
218 | }, this); |
---|
219 | }, |
---|
220 | |
---|
221 | /** |
---|
222 | * Find the Store's collection for a certain type of model. |
---|
223 | * @param {Backbone.RelationalModel} model |
---|
224 | * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null |
---|
225 | */ |
---|
226 | getCollection: function( model ) { |
---|
227 | if ( model instanceof Backbone.RelationalModel ) { |
---|
228 | model = model.constructor; |
---|
229 | } |
---|
230 | |
---|
231 | var rootModel = model; |
---|
232 | while ( rootModel._superModel ) { |
---|
233 | rootModel = rootModel._superModel; |
---|
234 | } |
---|
235 | |
---|
236 | var coll = _.detect( this._collections, function( c ) { |
---|
237 | return c.model === rootModel; |
---|
238 | }); |
---|
239 | |
---|
240 | if ( !coll ) { |
---|
241 | coll = this._createCollection( rootModel ); |
---|
242 | } |
---|
243 | |
---|
244 | return coll; |
---|
245 | }, |
---|
246 | |
---|
247 | /** |
---|
248 | * Find a type on the global object by name. Splits name on dots. |
---|
249 | * @param {String} name |
---|
250 | * @return {Object} |
---|
251 | */ |
---|
252 | getObjectByName: function( name ) { |
---|
253 | var parts = name.split( '.' ), |
---|
254 | type = null; |
---|
255 | |
---|
256 | _.find( this._modelScopes || [], function( scope ) { |
---|
257 | type = _.reduce( parts || [], function( memo, val ) { |
---|
258 | return memo ? memo[ val ] : undefined; |
---|
259 | }, scope ); |
---|
260 | |
---|
261 | if ( type && type !== scope ) { |
---|
262 | return true; |
---|
263 | } |
---|
264 | }, this ); |
---|
265 | |
---|
266 | return type; |
---|
267 | }, |
---|
268 | |
---|
269 | _createCollection: function( type ) { |
---|
270 | var coll; |
---|
271 | |
---|
272 | // If 'type' is an instance, take its constructor |
---|
273 | if ( type instanceof Backbone.RelationalModel ) { |
---|
274 | type = type.constructor; |
---|
275 | } |
---|
276 | |
---|
277 | // Type should inherit from Backbone.RelationalModel. |
---|
278 | if ( type.prototype instanceof Backbone.RelationalModel ) { |
---|
279 | coll = new Backbone.Collection(); |
---|
280 | coll.model = type; |
---|
281 | |
---|
282 | this._collections.push( coll ); |
---|
283 | } |
---|
284 | |
---|
285 | return coll; |
---|
286 | }, |
---|
287 | |
---|
288 | /** |
---|
289 | * Find the attribute that is to be used as the `id` on a given object |
---|
290 | * @param type |
---|
291 | * @param {String|Number|Object|Backbone.RelationalModel} item |
---|
292 | * @return {String|Number} |
---|
293 | */ |
---|
294 | resolveIdForItem: function( type, item ) { |
---|
295 | var id = _.isString( item ) || _.isNumber( item ) ? item : null; |
---|
296 | |
---|
297 | if ( id === null ) { |
---|
298 | if ( item instanceof Backbone.RelationalModel ) { |
---|
299 | id = item.id; |
---|
300 | } |
---|
301 | else if ( _.isObject( item ) ) { |
---|
302 | id = item[ type.prototype.idAttribute ]; |
---|
303 | } |
---|
304 | } |
---|
305 | |
---|
306 | // Make all falsy values `null` (except for 0, which could be an id.. see '/issues/179') |
---|
307 | if ( !id && id !== 0 ) { |
---|
308 | id = null; |
---|
309 | } |
---|
310 | |
---|
311 | return id; |
---|
312 | }, |
---|
313 | |
---|
314 | /** |
---|
315 | * |
---|
316 | * @param type |
---|
317 | * @param {String|Number|Object|Backbone.RelationalModel} item |
---|
318 | */ |
---|
319 | find: function( type, item ) { |
---|
320 | var id = this.resolveIdForItem( type, item ); |
---|
321 | var coll = this.getCollection( type ); |
---|
322 | |
---|
323 | // Because the found object could be of any of the type's superModel |
---|
324 | // types, only return it if it's actually of the type asked for. |
---|
325 | if ( coll ) { |
---|
326 | var obj = coll.get( id ); |
---|
327 | |
---|
328 | if ( obj instanceof type ) { |
---|
329 | return obj; |
---|
330 | } |
---|
331 | } |
---|
332 | |
---|
333 | return null; |
---|
334 | }, |
---|
335 | |
---|
336 | /** |
---|
337 | * Add a 'model' to it's appropriate collection. Retain the original contents of 'model.collection'. |
---|
338 | * @param {Backbone.RelationalModel} model |
---|
339 | */ |
---|
340 | register: function( model ) { |
---|
341 | var coll = this.getCollection( model ); |
---|
342 | |
---|
343 | if ( coll ) { |
---|
344 | if ( coll.get( model ) ) { |
---|
345 | throw new Error( "Cannot instantiate more than one Backbone.RelationalModel with the same id per type!" ); |
---|
346 | } |
---|
347 | |
---|
348 | var modelColl = model.collection; |
---|
349 | coll.add( model ); |
---|
350 | model.bind( 'destroy', this.unregister, this ); |
---|
351 | model.collection = modelColl; |
---|
352 | } |
---|
353 | }, |
---|
354 | |
---|
355 | /** |
---|
356 | * Explicitly update a model's id in it's store collection |
---|
357 | * @param {Backbone.RelationalModel} model |
---|
358 | */ |
---|
359 | update: function( model ) { |
---|
360 | var coll = this.getCollection( model ); |
---|
361 | coll._onModelEvent( 'change:' + model.idAttribute, model, coll ); |
---|
362 | }, |
---|
363 | |
---|
364 | /** |
---|
365 | * Remove a 'model' from the store. |
---|
366 | * @param {Backbone.RelationalModel} model |
---|
367 | */ |
---|
368 | unregister: function( model ) { |
---|
369 | model.unbind( 'destroy', this.unregister ); |
---|
370 | var coll = this.getCollection( model ); |
---|
371 | coll && coll.remove( model ); |
---|
372 | } |
---|
373 | }); |
---|
374 | Backbone.Relational.store = new Backbone.Store(); |
---|
375 | |
---|
376 | /** |
---|
377 | * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:<key>' events |
---|
378 | * are used to regulate addition and removal of models from relations. |
---|
379 | * |
---|
380 | * @param {Backbone.RelationalModel} instance |
---|
381 | * @param {Object} options |
---|
382 | * @param {string} options.key |
---|
383 | * @param {Backbone.RelationalModel.constructor} options.relatedModel |
---|
384 | * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids. |
---|
385 | * @param {Boolean} [options.createModels=true] Create objects from the contents of keys if the object is not found in Backbone.store. |
---|
386 | * @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate |
---|
387 | * the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs |
---|
388 | * {Backbone.Relation|String} type ('HasOne' or 'HasMany'). |
---|
389 | */ |
---|
390 | Backbone.Relation = function( instance, options ) { |
---|
391 | this.instance = instance; |
---|
392 | // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype |
---|
393 | options = _.isObject( options ) ? options : {}; |
---|
394 | this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation ); |
---|
395 | this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type : |
---|
396 | Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type ); |
---|
397 | this.model = options.model || this.instance.constructor; |
---|
398 | this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options ); |
---|
399 | |
---|
400 | this.key = this.options.key; |
---|
401 | this.keySource = this.options.keySource || this.key; |
---|
402 | this.keyDestination = this.options.keyDestination || this.keySource || this.key; |
---|
403 | |
---|
404 | // 'exports' should be the global object where 'relatedModel' can be found on if given as a string. |
---|
405 | this.relatedModel = this.options.relatedModel; |
---|
406 | if ( _.isString( this.relatedModel ) ) { |
---|
407 | this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel ); |
---|
408 | } |
---|
409 | |
---|
410 | if ( !this.checkPreconditions() ) { |
---|
411 | return; |
---|
412 | } |
---|
413 | |
---|
414 | if ( instance ) { |
---|
415 | var contentKey = this.keySource; |
---|
416 | if ( contentKey !== this.key && typeof this.instance.get( this.key ) === 'object' ) { |
---|
417 | contentKey = this.key; |
---|
418 | } |
---|
419 | |
---|
420 | this.keyContents = this.instance.get( contentKey ); |
---|
421 | |
---|
422 | // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'. |
---|
423 | if ( this.keySource !== this.key ) { |
---|
424 | this.instance.unset( this.keySource, { silent: true } ); |
---|
425 | } |
---|
426 | |
---|
427 | // Add this Relation to instance._relations |
---|
428 | this.instance._relations.push( this ); |
---|
429 | } |
---|
430 | |
---|
431 | // Add the reverse relation on 'relatedModel' to the store's reverseRelations |
---|
432 | if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) { |
---|
433 | Backbone.Relational.store.addReverseRelation( _.defaults( { |
---|
434 | isAutoRelation: true, |
---|
435 | model: this.relatedModel, |
---|
436 | relatedModel: this.model, |
---|
437 | reverseRelation: this.options // current relation is the 'reverseRelation' for it's own reverseRelation |
---|
438 | }, |
---|
439 | this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.) |
---|
440 | ) ); |
---|
441 | } |
---|
442 | |
---|
443 | _.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' ); |
---|
444 | |
---|
445 | if ( instance ) { |
---|
446 | this.initialize(); |
---|
447 | |
---|
448 | if ( options.autoFetch ) { |
---|
449 | this.instance.fetchRelated( options.key, _.isObject( options.autoFetch ) ? options.autoFetch : {} ); |
---|
450 | } |
---|
451 | |
---|
452 | // When a model in the store is destroyed, check if it is 'this.instance'. |
---|
453 | Backbone.Relational.store.getCollection( this.instance ) |
---|
454 | .bind( 'relational:remove', this._modelRemovedFromCollection ); |
---|
455 | |
---|
456 | // When 'relatedModel' are created or destroyed, check if it affects this relation. |
---|
457 | Backbone.Relational.store.getCollection( this.relatedModel ) |
---|
458 | .bind( 'relational:add', this._relatedModelAdded ) |
---|
459 | .bind( 'relational:remove', this._relatedModelRemoved ); |
---|
460 | } |
---|
461 | }; |
---|
462 | // Fix inheritance :\ |
---|
463 | Backbone.Relation.extend = Backbone.Model.extend; |
---|
464 | // Set up all inheritable **Backbone.Relation** properties and methods. |
---|
465 | _.extend( Backbone.Relation.prototype, Backbone.Events, Backbone.Semaphore, { |
---|
466 | options: { |
---|
467 | createModels: true, |
---|
468 | includeInJSON: true, |
---|
469 | isAutoRelation: false, |
---|
470 | autoFetch: false |
---|
471 | }, |
---|
472 | |
---|
473 | instance: null, |
---|
474 | key: null, |
---|
475 | keyContents: null, |
---|
476 | relatedModel: null, |
---|
477 | reverseRelation: null, |
---|
478 | related: null, |
---|
479 | |
---|
480 | _relatedModelAdded: function( model, coll, options ) { |
---|
481 | // Allow 'model' to set up it's relations, before calling 'tryAddRelated' |
---|
482 | // (which can result in a call to 'addRelated' on a relation of 'model') |
---|
483 | var dit = this; |
---|
484 | model.queue( function() { |
---|
485 | dit.tryAddRelated( model, options ); |
---|
486 | }); |
---|
487 | }, |
---|
488 | |
---|
489 | _relatedModelRemoved: function( model, coll, options ) { |
---|
490 | this.removeRelated( model, options ); |
---|
491 | }, |
---|
492 | |
---|
493 | _modelRemovedFromCollection: function( model ) { |
---|
494 | if ( model === this.instance ) { |
---|
495 | this.destroy(); |
---|
496 | } |
---|
497 | }, |
---|
498 | |
---|
499 | /** |
---|
500 | * Check several pre-conditions. |
---|
501 | * @return {Boolean} True if pre-conditions are satisfied, false if they're not. |
---|
502 | */ |
---|
503 | checkPreconditions: function() { |
---|
504 | var i = this.instance, |
---|
505 | k = this.key, |
---|
506 | m = this.model, |
---|
507 | rm = this.relatedModel, |
---|
508 | warn = Backbone.Relational.showWarnings && typeof console !== 'undefined'; |
---|
509 | |
---|
510 | if ( !m || !k || !rm ) { |
---|
511 | warn && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, m, k, rm ); |
---|
512 | return false; |
---|
513 | } |
---|
514 | // Check if the type in 'model' inherits from Backbone.RelationalModel |
---|
515 | if ( !( m.prototype instanceof Backbone.RelationalModel ) ) { |
---|
516 | warn && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i ); |
---|
517 | return false; |
---|
518 | } |
---|
519 | // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel |
---|
520 | if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) { |
---|
521 | warn && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm ); |
---|
522 | return false; |
---|
523 | } |
---|
524 | // Check if this is not a HasMany, and the reverse relation is HasMany as well |
---|
525 | if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) { |
---|
526 | warn && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this ); |
---|
527 | return false; |
---|
528 | } |
---|
529 | |
---|
530 | // Check if we're not attempting to create a duplicate relationship |
---|
531 | if ( i && i._relations.length ) { |
---|
532 | var exists = _.any( i._relations || [], function( rel ) { |
---|
533 | var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key; |
---|
534 | return rel.relatedModel === rm && rel.key === k && |
---|
535 | ( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key ); |
---|
536 | }, this ); |
---|
537 | |
---|
538 | if ( exists ) { |
---|
539 | warn && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists', |
---|
540 | this, i, k, rm, this.reverseRelation.key ); |
---|
541 | return false; |
---|
542 | } |
---|
543 | } |
---|
544 | |
---|
545 | return true; |
---|
546 | }, |
---|
547 | |
---|
548 | /** |
---|
549 | * Set the related model(s) for this relation |
---|
550 | * @param {Backbone.Model|Backbone.Collection} related |
---|
551 | * @param {Object} [options] |
---|
552 | */ |
---|
553 | setRelated: function( related, options ) { |
---|
554 | this.related = related; |
---|
555 | |
---|
556 | this.instance.acquire(); |
---|
557 | this.instance.attributes[ this.key ] = related; |
---|
558 | this.instance.release(); |
---|
559 | }, |
---|
560 | |
---|
561 | /** |
---|
562 | * Determine if a relation (on a different RelationalModel) is the reverse |
---|
563 | * relation of the current one. |
---|
564 | * @param {Backbone.Relation} relation |
---|
565 | * @return {Boolean} |
---|
566 | */ |
---|
567 | _isReverseRelation: function( relation ) { |
---|
568 | if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key && |
---|
569 | this.key === relation.reverseRelation.key ) { |
---|
570 | return true; |
---|
571 | } |
---|
572 | return false; |
---|
573 | }, |
---|
574 | |
---|
575 | /** |
---|
576 | * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s). |
---|
577 | * @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model. |
---|
578 | * If not specified, 'this.related' is used. |
---|
579 | * @return {Backbone.Relation[]} |
---|
580 | */ |
---|
581 | getReverseRelations: function( model ) { |
---|
582 | var reverseRelations = []; |
---|
583 | // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array. |
---|
584 | var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] ); |
---|
585 | _.each( models || [], function( related ) { |
---|
586 | _.each( related.getRelations() || [], function( relation ) { |
---|
587 | if ( this._isReverseRelation( relation ) ) { |
---|
588 | reverseRelations.push( relation ); |
---|
589 | } |
---|
590 | }, this ); |
---|
591 | }, this ); |
---|
592 | |
---|
593 | return reverseRelations; |
---|
594 | }, |
---|
595 | |
---|
596 | /** |
---|
597 | * Rename options.silent to options.silentChange, so events propagate properly. |
---|
598 | * (for example in HasMany, from 'addRelated'->'handleAddition') |
---|
599 | * @param {Object} [options] |
---|
600 | * @return {Object} |
---|
601 | */ |
---|
602 | sanitizeOptions: function( options ) { |
---|
603 | options = options ? _.clone( options ) : {}; |
---|
604 | if ( options.silent ) { |
---|
605 | options.silentChange = true; |
---|
606 | delete options.silent; |
---|
607 | } |
---|
608 | return options; |
---|
609 | }, |
---|
610 | |
---|
611 | /** |
---|
612 | * Rename options.silentChange to options.silent, so events are silenced as intended in Backbone's |
---|
613 | * original functions. |
---|
614 | * @param {Object} [options] |
---|
615 | * @return {Object} |
---|
616 | */ |
---|
617 | unsanitizeOptions: function( options ) { |
---|
618 | options = options ? _.clone( options ) : {}; |
---|
619 | if ( options.silentChange ) { |
---|
620 | options.silent = true; |
---|
621 | delete options.silentChange; |
---|
622 | } |
---|
623 | return options; |
---|
624 | }, |
---|
625 | |
---|
626 | // Cleanup. Get reverse relation, call removeRelated on each. |
---|
627 | destroy: function() { |
---|
628 | Backbone.Relational.store.getCollection( this.instance ) |
---|
629 | .unbind( 'relational:remove', this._modelRemovedFromCollection ); |
---|
630 | |
---|
631 | Backbone.Relational.store.getCollection( this.relatedModel ) |
---|
632 | .unbind( 'relational:add', this._relatedModelAdded ) |
---|
633 | .unbind( 'relational:remove', this._relatedModelRemoved ); |
---|
634 | |
---|
635 | _.each( this.getReverseRelations() || [], function( relation ) { |
---|
636 | relation.removeRelated( this.instance ); |
---|
637 | }, this ); |
---|
638 | } |
---|
639 | }); |
---|
640 | |
---|
641 | Backbone.HasOne = Backbone.Relation.extend({ |
---|
642 | options: { |
---|
643 | reverseRelation: { type: 'HasMany' } |
---|
644 | }, |
---|
645 | |
---|
646 | initialize: function() { |
---|
647 | _.bindAll( this, 'onChange' ); |
---|
648 | |
---|
649 | this.instance.bind( 'relational:change:' + this.key, this.onChange ); |
---|
650 | |
---|
651 | var model = this.findRelated( { silent: true } ); |
---|
652 | this.setRelated( model ); |
---|
653 | |
---|
654 | // Notify new 'related' object of the new relation. |
---|
655 | _.each( this.getReverseRelations() || [], function( relation ) { |
---|
656 | relation.addRelated( this.instance ); |
---|
657 | }, this ); |
---|
658 | }, |
---|
659 | |
---|
660 | findRelated: function( options ) { |
---|
661 | var item = this.keyContents; |
---|
662 | var model = null; |
---|
663 | |
---|
664 | if ( item instanceof this.relatedModel ) { |
---|
665 | model = item; |
---|
666 | } |
---|
667 | else if ( item || item === 0 ) { // since 0 can be a valid `id` as well |
---|
668 | model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } ); |
---|
669 | } |
---|
670 | |
---|
671 | return model; |
---|
672 | }, |
---|
673 | |
---|
674 | /** |
---|
675 | * If the key is changed, notify old & new reverse relations and initialize the new relation |
---|
676 | */ |
---|
677 | onChange: function( model, attr, options ) { |
---|
678 | // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange) |
---|
679 | if ( this.isLocked() ) { |
---|
680 | return; |
---|
681 | } |
---|
682 | this.acquire(); |
---|
683 | options = this.sanitizeOptions( options ); |
---|
684 | |
---|
685 | // 'options._related' is set by 'addRelated'/'removeRelated'. If it is set, the change |
---|
686 | // is the result of a call from a relation. If it's not, the change is the result of |
---|
687 | // a 'set' call on this.instance. |
---|
688 | var changed = _.isUndefined( options._related ); |
---|
689 | var oldRelated = changed ? this.related : options._related; |
---|
690 | |
---|
691 | if ( changed ) { |
---|
692 | this.keyContents = attr; |
---|
693 | |
---|
694 | // Set new 'related' |
---|
695 | if ( attr instanceof this.relatedModel ) { |
---|
696 | this.related = attr; |
---|
697 | } |
---|
698 | else if ( attr ) { |
---|
699 | var related = this.findRelated( options ); |
---|
700 | this.setRelated( related ); |
---|
701 | } |
---|
702 | else { |
---|
703 | this.setRelated( null ); |
---|
704 | } |
---|
705 | } |
---|
706 | |
---|
707 | // Notify old 'related' object of the terminated relation |
---|
708 | if ( oldRelated && this.related !== oldRelated ) { |
---|
709 | _.each( this.getReverseRelations( oldRelated ) || [], function( relation ) { |
---|
710 | relation.removeRelated( this.instance, options ); |
---|
711 | }, this ); |
---|
712 | } |
---|
713 | |
---|
714 | // Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated; |
---|
715 | // that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'. |
---|
716 | // In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet. |
---|
717 | _.each( this.getReverseRelations() || [], function( relation ) { |
---|
718 | relation.addRelated( this.instance, options ); |
---|
719 | }, this); |
---|
720 | |
---|
721 | // Fire the 'update:<key>' event if 'related' was updated |
---|
722 | if ( !options.silentChange && this.related !== oldRelated ) { |
---|
723 | var dit = this; |
---|
724 | Backbone.Relational.eventQueue.add( function() { |
---|
725 | dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options ); |
---|
726 | }); |
---|
727 | } |
---|
728 | this.release(); |
---|
729 | }, |
---|
730 | |
---|
731 | /** |
---|
732 | * If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents' |
---|
733 | */ |
---|
734 | tryAddRelated: function( model, options ) { |
---|
735 | if ( this.related ) { |
---|
736 | return; |
---|
737 | } |
---|
738 | options = this.sanitizeOptions( options ); |
---|
739 | |
---|
740 | var item = this.keyContents; |
---|
741 | if ( item || item === 0 ) { // since 0 can be a valid `id` as well |
---|
742 | var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item ); |
---|
743 | if ( !_.isNull( id ) && model.id === id ) { |
---|
744 | this.addRelated( model, options ); |
---|
745 | } |
---|
746 | } |
---|
747 | }, |
---|
748 | |
---|
749 | addRelated: function( model, options ) { |
---|
750 | if ( model !== this.related ) { |
---|
751 | var oldRelated = this.related || null; |
---|
752 | this.setRelated( model ); |
---|
753 | this.onChange( this.instance, model, { _related: oldRelated } ); |
---|
754 | } |
---|
755 | }, |
---|
756 | |
---|
757 | removeRelated: function( model, options ) { |
---|
758 | if ( !this.related ) { |
---|
759 | return; |
---|
760 | } |
---|
761 | |
---|
762 | if ( model === this.related ) { |
---|
763 | var oldRelated = this.related || null; |
---|
764 | this.setRelated( null ); |
---|
765 | this.onChange( this.instance, model, { _related: oldRelated } ); |
---|
766 | } |
---|
767 | } |
---|
768 | }); |
---|
769 | |
---|
770 | Backbone.HasMany = Backbone.Relation.extend({ |
---|
771 | collectionType: null, |
---|
772 | |
---|
773 | options: { |
---|
774 | reverseRelation: { type: 'HasOne' }, |
---|
775 | collectionType: Backbone.Collection, |
---|
776 | collectionKey: true, |
---|
777 | collectionOptions: {} |
---|
778 | }, |
---|
779 | |
---|
780 | initialize: function() { |
---|
781 | _.bindAll( this, 'onChange', 'handleAddition', 'handleRemoval', 'handleReset' ); |
---|
782 | this.instance.bind( 'relational:change:' + this.key, this.onChange ); |
---|
783 | |
---|
784 | // Handle a custom 'collectionType' |
---|
785 | this.collectionType = this.options.collectionType; |
---|
786 | if ( _.isString( this.collectionType ) ) { |
---|
787 | this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType ); |
---|
788 | } |
---|
789 | if ( !this.collectionType.prototype instanceof Backbone.Collection ){ |
---|
790 | throw new Error( 'collectionType must inherit from Backbone.Collection' ); |
---|
791 | } |
---|
792 | |
---|
793 | // Handle cases where a model/relation is created with a collection passed straight into 'attributes' |
---|
794 | if ( this.keyContents instanceof Backbone.Collection ) { |
---|
795 | this.setRelated( this._prepareCollection( this.keyContents ) ); |
---|
796 | } |
---|
797 | else { |
---|
798 | this.setRelated( this._prepareCollection() ); |
---|
799 | } |
---|
800 | |
---|
801 | this.findRelated( { silent: true } ); |
---|
802 | }, |
---|
803 | |
---|
804 | _getCollectionOptions: function() { |
---|
805 | return _.isFunction( this.options.collectionOptions ) ? |
---|
806 | this.options.collectionOptions( this.instance ) : |
---|
807 | this.options.collectionOptions; |
---|
808 | }, |
---|
809 | |
---|
810 | /** |
---|
811 | * Bind events and setup collectionKeys for a collection that is to be used as the backing store for a HasMany. |
---|
812 | * If no 'collection' is supplied, a new collection will be created of the specified 'collectionType' option. |
---|
813 | * @param {Backbone.Collection} [collection] |
---|
814 | */ |
---|
815 | _prepareCollection: function( collection ) { |
---|
816 | if ( this.related ) { |
---|
817 | this.related |
---|
818 | .unbind( 'relational:add', this.handleAddition ) |
---|
819 | .unbind( 'relational:remove', this.handleRemoval ) |
---|
820 | .unbind( 'relational:reset', this.handleReset ) |
---|
821 | } |
---|
822 | |
---|
823 | if ( !collection || !( collection instanceof Backbone.Collection ) ) { |
---|
824 | collection = new this.collectionType( [], this._getCollectionOptions() ); |
---|
825 | } |
---|
826 | |
---|
827 | collection.model = this.relatedModel; |
---|
828 | |
---|
829 | if ( this.options.collectionKey ) { |
---|
830 | var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey; |
---|
831 | |
---|
832 | if ( collection[ key ] && collection[ key ] !== this.instance ) { |
---|
833 | if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) { |
---|
834 | console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey ); |
---|
835 | } |
---|
836 | } |
---|
837 | else if ( key ) { |
---|
838 | collection[ key ] = this.instance; |
---|
839 | } |
---|
840 | } |
---|
841 | |
---|
842 | collection |
---|
843 | .bind( 'relational:add', this.handleAddition ) |
---|
844 | .bind( 'relational:remove', this.handleRemoval ) |
---|
845 | .bind( 'relational:reset', this.handleReset ); |
---|
846 | |
---|
847 | return collection; |
---|
848 | }, |
---|
849 | |
---|
850 | findRelated: function( options ) { |
---|
851 | if ( this.keyContents ) { |
---|
852 | var models = []; |
---|
853 | |
---|
854 | if ( this.keyContents instanceof Backbone.Collection ) { |
---|
855 | models = this.keyContents.models; |
---|
856 | } |
---|
857 | else { |
---|
858 | // Handle cases the an API/user supplies just an Object/id instead of an Array |
---|
859 | this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ]; |
---|
860 | |
---|
861 | // Try to find instances of the appropriate 'relatedModel' in the store |
---|
862 | _.each( this.keyContents || [], function( item ) { |
---|
863 | var model = null; |
---|
864 | if ( item instanceof this.relatedModel ) { |
---|
865 | model = item; |
---|
866 | } |
---|
867 | else if ( item || item === 0 ) { // since 0 can be a valid `id` as well |
---|
868 | model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } ); |
---|
869 | } |
---|
870 | |
---|
871 | if ( model && !this.related.get( model ) ) { |
---|
872 | models.push( model ); |
---|
873 | } |
---|
874 | }, this ); |
---|
875 | } |
---|
876 | |
---|
877 | // Add all found 'models' in on go, so 'add' will only be called once (and thus 'sort', etc.) |
---|
878 | if ( models.length ) { |
---|
879 | options = this.unsanitizeOptions( options ); |
---|
880 | this.related.add( models, options ); |
---|
881 | } |
---|
882 | } |
---|
883 | }, |
---|
884 | |
---|
885 | /** |
---|
886 | * If the key is changed, notify old & new reverse relations and initialize the new relation |
---|
887 | */ |
---|
888 | onChange: function( model, attr, options ) { |
---|
889 | options = this.sanitizeOptions( options ); |
---|
890 | this.keyContents = attr; |
---|
891 | |
---|
892 | // Replace 'this.related' by 'attr' if it is a Backbone.Collection |
---|
893 | if ( attr instanceof Backbone.Collection ) { |
---|
894 | this._prepareCollection( attr ); |
---|
895 | this.related = attr; |
---|
896 | } |
---|
897 | // Otherwise, 'attr' should be an array of related object ids. |
---|
898 | // Re-use the current 'this.related' if it is a Backbone.Collection, and remove any current entries. |
---|
899 | // Otherwise, create a new collection. |
---|
900 | else { |
---|
901 | var oldIds = {}, newIds = {}; |
---|
902 | |
---|
903 | if (!_.isArray( attr ) && attr !== undefined) { |
---|
904 | attr = [ attr ]; |
---|
905 | } |
---|
906 | |
---|
907 | _.each( attr, function( attributes ) { |
---|
908 | newIds[ attributes.id ] = true; |
---|
909 | }); |
---|
910 | |
---|
911 | var coll = this.related; |
---|
912 | if ( coll instanceof Backbone.Collection ) { |
---|
913 | // Make sure to operate on a copy since we're removing while iterating |
---|
914 | _.each( coll.models.slice(0) , function( model ) { |
---|
915 | // When fetch is called with the 'keepNewModels' option, we don't want to remove |
---|
916 | // client-created new models when the fetch is completed. |
---|
917 | if ( !options.keepNewModels || !model.isNew() ) { |
---|
918 | oldIds[ model.id ] = true; |
---|
919 | coll.remove( model, { silent: (model.id in newIds) } ); |
---|
920 | } |
---|
921 | }); |
---|
922 | } else { |
---|
923 | coll = this._prepareCollection(); |
---|
924 | } |
---|
925 | |
---|
926 | _.each( attr, function( attributes ) { |
---|
927 | var model = this.relatedModel.findOrCreate( attributes, { create: this.options.createModels } ); |
---|
928 | if (model) { |
---|
929 | coll.add( model, { silent: (attributes.id in oldIds)} ); |
---|
930 | } |
---|
931 | }, this); |
---|
932 | |
---|
933 | this.setRelated( coll ); |
---|
934 | |
---|
935 | } |
---|
936 | |
---|
937 | var dit = this; |
---|
938 | Backbone.Relational.eventQueue.add( function() { |
---|
939 | !options.silentChange && dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options ); |
---|
940 | }); |
---|
941 | }, |
---|
942 | |
---|
943 | tryAddRelated: function( model, options ) { |
---|
944 | options = this.sanitizeOptions( options ); |
---|
945 | if ( !this.related.get( model ) ) { |
---|
946 | // Check if this new model was specified in 'this.keyContents' |
---|
947 | var item = _.any( this.keyContents || [], function( item ) { |
---|
948 | var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item ); |
---|
949 | return !_.isNull( id ) && id === model.id; |
---|
950 | }, this ); |
---|
951 | |
---|
952 | if ( item ) { |
---|
953 | this.related.add( model, options ); |
---|
954 | } |
---|
955 | } |
---|
956 | }, |
---|
957 | |
---|
958 | /** |
---|
959 | * When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations. |
---|
960 | * (should be 'HasOne', must set 'this.instance' as their related). |
---|
961 | */ |
---|
962 | handleAddition: function( model, coll, options ) { |
---|
963 | //console.debug('handleAddition called; args=%o', arguments); |
---|
964 | // Make sure the model is in fact a valid model before continuing. |
---|
965 | // (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel) |
---|
966 | if ( !( model instanceof Backbone.Model ) ) { |
---|
967 | return; |
---|
968 | } |
---|
969 | |
---|
970 | options = this.sanitizeOptions( options ); |
---|
971 | |
---|
972 | _.each( this.getReverseRelations( model ) || [], function( relation ) { |
---|
973 | relation.addRelated( this.instance, options ); |
---|
974 | }, this ); |
---|
975 | |
---|
976 | // Only trigger 'add' once the newly added model is initialized (so, has it's relations set up) |
---|
977 | var dit = this; |
---|
978 | Backbone.Relational.eventQueue.add( function() { |
---|
979 | !options.silentChange && dit.instance.trigger( 'add:' + dit.key, model, dit.related, options ); |
---|
980 | }); |
---|
981 | }, |
---|
982 | |
---|
983 | /** |
---|
984 | * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations. |
---|
985 | * (should be 'HasOne', which should be nullified) |
---|
986 | */ |
---|
987 | handleRemoval: function( model, coll, options ) { |
---|
988 | //console.debug('handleRemoval called; args=%o', arguments); |
---|
989 | if ( !( model instanceof Backbone.Model ) ) { |
---|
990 | return; |
---|
991 | } |
---|
992 | |
---|
993 | options = this.sanitizeOptions( options ); |
---|
994 | |
---|
995 | _.each( this.getReverseRelations( model ) || [], function( relation ) { |
---|
996 | relation.removeRelated( this.instance, options ); |
---|
997 | }, this ); |
---|
998 | |
---|
999 | var dit = this; |
---|
1000 | Backbone.Relational.eventQueue.add( function() { |
---|
1001 | !options.silentChange && dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options ); |
---|
1002 | }); |
---|
1003 | }, |
---|
1004 | |
---|
1005 | handleReset: function( coll, options ) { |
---|
1006 | options = this.sanitizeOptions( options ); |
---|
1007 | |
---|
1008 | var dit = this; |
---|
1009 | Backbone.Relational.eventQueue.add( function() { |
---|
1010 | !options.silentChange && dit.instance.trigger( 'reset:' + dit.key, dit.related, options ); |
---|
1011 | }); |
---|
1012 | }, |
---|
1013 | |
---|
1014 | addRelated: function( model, options ) { |
---|
1015 | var dit = this; |
---|
1016 | options = this.unsanitizeOptions( options ); |
---|
1017 | model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice |
---|
1018 | if ( dit.related && !dit.related.get( model ) ) { |
---|
1019 | dit.related.add( model, options ); |
---|
1020 | } |
---|
1021 | }); |
---|
1022 | }, |
---|
1023 | |
---|
1024 | removeRelated: function( model, options ) { |
---|
1025 | options = this.unsanitizeOptions( options ); |
---|
1026 | if ( this.related.get( model ) ) { |
---|
1027 | this.related.remove( model, options ); |
---|
1028 | } |
---|
1029 | } |
---|
1030 | }); |
---|
1031 | |
---|
1032 | /** |
---|
1033 | * A type of Backbone.Model that also maintains relations to other models and collections. |
---|
1034 | * New events when compared to the original: |
---|
1035 | * - 'add:<key>' (model, related collection, options) |
---|
1036 | * - 'remove:<key>' (model, related collection, options) |
---|
1037 | * - 'update:<key>' (model, related model or collection, options) |
---|
1038 | */ |
---|
1039 | Backbone.RelationalModel = Backbone.Model.extend({ |
---|
1040 | relations: null, // Relation descriptions on the prototype |
---|
1041 | _relations: null, // Relation instances |
---|
1042 | _isInitialized: false, |
---|
1043 | _deferProcessing: false, |
---|
1044 | _queue: null, |
---|
1045 | |
---|
1046 | subModelTypeAttribute: 'type', |
---|
1047 | subModelTypes: null, |
---|
1048 | |
---|
1049 | constructor: function( attributes, options ) { |
---|
1050 | // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'. |
---|
1051 | // Defer 'processQueue', so that when 'Relation.createModels' is used we: |
---|
1052 | // a) Survive 'Backbone.Collection.add'; this takes care we won't error on "can't add model to a set twice" |
---|
1053 | // (by creating a model from properties, having the model add itself to the collection via one of |
---|
1054 | // it's relations, then trying to add it to the collection). |
---|
1055 | // b) Trigger 'HasMany' collection events only after the model is really fully set up. |
---|
1056 | // Example that triggers both a and b: "p.get('jobs').add( { company: c, person: p } )". |
---|
1057 | var dit = this; |
---|
1058 | if ( options && options.collection ) { |
---|
1059 | this._deferProcessing = true; |
---|
1060 | |
---|
1061 | var processQueue = function( model ) { |
---|
1062 | if ( model === dit ) { |
---|
1063 | dit._deferProcessing = false; |
---|
1064 | dit.processQueue(); |
---|
1065 | options.collection.unbind( 'relational:add', processQueue ); |
---|
1066 | } |
---|
1067 | }; |
---|
1068 | options.collection.bind( 'relational:add', processQueue ); |
---|
1069 | |
---|
1070 | // So we do process the queue eventually, regardless of whether this model really gets added to 'options.collection'. |
---|
1071 | _.defer( function() { |
---|
1072 | processQueue( dit ); |
---|
1073 | }); |
---|
1074 | } |
---|
1075 | |
---|
1076 | this._queue = new Backbone.BlockingQueue(); |
---|
1077 | this._queue.block(); |
---|
1078 | Backbone.Relational.eventQueue.block(); |
---|
1079 | |
---|
1080 | Backbone.Model.apply( this, arguments ); |
---|
1081 | |
---|
1082 | // Try to run the global queue holding external events |
---|
1083 | Backbone.Relational.eventQueue.unblock(); |
---|
1084 | }, |
---|
1085 | |
---|
1086 | /** |
---|
1087 | * Override 'trigger' to queue 'change' and 'change:*' events |
---|
1088 | */ |
---|
1089 | trigger: function( eventName ) { |
---|
1090 | if ( eventName.length > 5 && 'change' === eventName.substr( 0, 6 ) ) { |
---|
1091 | var dit = this, args = arguments; |
---|
1092 | Backbone.Relational.eventQueue.add( function() { |
---|
1093 | Backbone.Model.prototype.trigger.apply( dit, args ); |
---|
1094 | }); |
---|
1095 | } |
---|
1096 | else { |
---|
1097 | Backbone.Model.prototype.trigger.apply( this, arguments ); |
---|
1098 | } |
---|
1099 | |
---|
1100 | return this; |
---|
1101 | }, |
---|
1102 | |
---|
1103 | /** |
---|
1104 | * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance. |
---|
1105 | * Invoked in the first call so 'set' (which is made from the Backbone.Model constructor). |
---|
1106 | */ |
---|
1107 | initializeRelations: function() { |
---|
1108 | this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once |
---|
1109 | this._relations = []; |
---|
1110 | |
---|
1111 | _.each( this.relations || [], function( rel ) { |
---|
1112 | var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type ); |
---|
1113 | if ( type && type.prototype instanceof Backbone.Relation ) { |
---|
1114 | new type( this, rel ); // Also pushes the new Relation into _relations |
---|
1115 | } |
---|
1116 | else { |
---|
1117 | Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid type!', rel ); |
---|
1118 | } |
---|
1119 | }, this ); |
---|
1120 | |
---|
1121 | this._isInitialized = true; |
---|
1122 | this.release(); |
---|
1123 | this.processQueue(); |
---|
1124 | }, |
---|
1125 | |
---|
1126 | /** |
---|
1127 | * When new values are set, notify this model's relations (also if options.silent is set). |
---|
1128 | * (Relation.setRelated locks this model before calling 'set' on it to prevent loops) |
---|
1129 | */ |
---|
1130 | updateRelations: function( options ) { |
---|
1131 | if ( this._isInitialized && !this.isLocked() ) { |
---|
1132 | _.each( this._relations || [], function( rel ) { |
---|
1133 | // Update from data in `rel.keySource` if set, or `rel.key` otherwise |
---|
1134 | var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ]; |
---|
1135 | if ( rel.related !== val ) { |
---|
1136 | this.trigger( 'relational:change:' + rel.key, this, val, options || {} ); |
---|
1137 | } |
---|
1138 | }, this ); |
---|
1139 | } |
---|
1140 | }, |
---|
1141 | |
---|
1142 | /** |
---|
1143 | * Either add to the queue (if we're not initialized yet), or execute right away. |
---|
1144 | */ |
---|
1145 | queue: function( func ) { |
---|
1146 | this._queue.add( func ); |
---|
1147 | }, |
---|
1148 | |
---|
1149 | /** |
---|
1150 | * Process _queue |
---|
1151 | */ |
---|
1152 | processQueue: function() { |
---|
1153 | if ( this._isInitialized && !this._deferProcessing && this._queue.isBlocked() ) { |
---|
1154 | this._queue.unblock(); |
---|
1155 | } |
---|
1156 | }, |
---|
1157 | |
---|
1158 | /** |
---|
1159 | * Get a specific relation. |
---|
1160 | * @param key {string} The relation key to look for. |
---|
1161 | * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'key', or null. |
---|
1162 | */ |
---|
1163 | getRelation: function( key ) { |
---|
1164 | return _.detect( this._relations, function( rel ) { |
---|
1165 | if ( rel.key === key ) { |
---|
1166 | return true; |
---|
1167 | } |
---|
1168 | }, this ); |
---|
1169 | }, |
---|
1170 | |
---|
1171 | /** |
---|
1172 | * Get all of the created relations. |
---|
1173 | * @return {Backbone.Relation[]} |
---|
1174 | */ |
---|
1175 | getRelations: function() { |
---|
1176 | return this._relations; |
---|
1177 | }, |
---|
1178 | |
---|
1179 | /** |
---|
1180 | * Retrieve related objects. |
---|
1181 | * @param key {string} The relation key to fetch models for. |
---|
1182 | * @param [options] {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'. |
---|
1183 | * @param [update=false] {boolean} Whether to force a fetch from the server (updating existing models). |
---|
1184 | * @return {jQuery.when[]} An array of request objects |
---|
1185 | */ |
---|
1186 | fetchRelated: function( key, options, update ) { |
---|
1187 | options || ( options = {} ); |
---|
1188 | var setUrl, |
---|
1189 | requests = [], |
---|
1190 | rel = this.getRelation( key ), |
---|
1191 | keyContents = rel && rel.keyContents, |
---|
1192 | toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) { |
---|
1193 | var id = Backbone.Relational.store.resolveIdForItem( rel.relatedModel, item ); |
---|
1194 | return !_.isNull( id ) && ( update || !Backbone.Relational.store.find( rel.relatedModel, id ) ); |
---|
1195 | }, this ); |
---|
1196 | |
---|
1197 | if ( toFetch && toFetch.length ) { |
---|
1198 | // Create a model for each entry in 'keyContents' that is to be fetched |
---|
1199 | var models = _.map( toFetch, function( item ) { |
---|
1200 | var model; |
---|
1201 | |
---|
1202 | if ( _.isObject( item ) ) { |
---|
1203 | model = rel.relatedModel.findOrCreate( item ); |
---|
1204 | } |
---|
1205 | else { |
---|
1206 | var attrs = {}; |
---|
1207 | attrs[ rel.relatedModel.prototype.idAttribute ] = item; |
---|
1208 | model = rel.relatedModel.findOrCreate( attrs ); |
---|
1209 | } |
---|
1210 | |
---|
1211 | return model; |
---|
1212 | }, this ); |
---|
1213 | |
---|
1214 | // Try if the 'collection' can provide a url to fetch a set of models in one request. |
---|
1215 | if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) { |
---|
1216 | setUrl = rel.related.url( models ); |
---|
1217 | } |
---|
1218 | |
---|
1219 | // An assumption is that when 'Backbone.Collection.url' is a function, it can handle building of set urls. |
---|
1220 | // To make sure it can, test if the url we got by supplying a list of models to fetch is different from |
---|
1221 | // the one supplied for the default fetch action (without args to 'url'). |
---|
1222 | if ( setUrl && setUrl !== rel.related.url() ) { |
---|
1223 | var opts = _.defaults( |
---|
1224 | { |
---|
1225 | error: function() { |
---|
1226 | var args = arguments; |
---|
1227 | _.each( models || [], function( model ) { |
---|
1228 | model.trigger( 'destroy', model, model.collection, options ); |
---|
1229 | options.error && options.error.apply( model, args ); |
---|
1230 | }); |
---|
1231 | }, |
---|
1232 | url: setUrl |
---|
1233 | }, |
---|
1234 | options, |
---|
1235 | { add: true } |
---|
1236 | ); |
---|
1237 | |
---|
1238 | requests = [ rel.related.fetch( opts ) ]; |
---|
1239 | } |
---|
1240 | else { |
---|
1241 | requests = _.map( models || [], function( model ) { |
---|
1242 | var opts = _.defaults( |
---|
1243 | { |
---|
1244 | error: function() { |
---|
1245 | model.trigger( 'destroy', model, model.collection, options ); |
---|
1246 | options.error && options.error.apply( model, arguments ); |
---|
1247 | } |
---|
1248 | }, |
---|
1249 | options |
---|
1250 | ); |
---|
1251 | return model.fetch( opts ); |
---|
1252 | }, this ); |
---|
1253 | } |
---|
1254 | } |
---|
1255 | |
---|
1256 | return requests; |
---|
1257 | }, |
---|
1258 | |
---|
1259 | set: function( key, value, options ) { |
---|
1260 | Backbone.Relational.eventQueue.block(); |
---|
1261 | |
---|
1262 | // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object |
---|
1263 | var attributes; |
---|
1264 | if ( _.isObject( key ) || key == null ) { |
---|
1265 | attributes = key; |
---|
1266 | options = value; |
---|
1267 | } |
---|
1268 | else { |
---|
1269 | attributes = {}; |
---|
1270 | attributes[ key ] = value; |
---|
1271 | } |
---|
1272 | |
---|
1273 | var result = Backbone.Model.prototype.set.apply( this, arguments ); |
---|
1274 | |
---|
1275 | // Ideal place to set up relations :) |
---|
1276 | if ( !this._isInitialized && !this.isLocked() ) { |
---|
1277 | this.constructor.initializeModelHierarchy(); |
---|
1278 | |
---|
1279 | Backbone.Relational.store.register( this ); |
---|
1280 | |
---|
1281 | this.initializeRelations(); |
---|
1282 | } |
---|
1283 | // Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true} |
---|
1284 | else if ( attributes && this.idAttribute in attributes ) { |
---|
1285 | Backbone.Relational.store.update( this ); |
---|
1286 | } |
---|
1287 | |
---|
1288 | if ( attributes ) { |
---|
1289 | this.updateRelations( options ); |
---|
1290 | } |
---|
1291 | |
---|
1292 | // Try to run the global queue holding external events |
---|
1293 | Backbone.Relational.eventQueue.unblock(); |
---|
1294 | |
---|
1295 | return result; |
---|
1296 | }, |
---|
1297 | |
---|
1298 | unset: function( attribute, options ) { |
---|
1299 | Backbone.Relational.eventQueue.block(); |
---|
1300 | |
---|
1301 | var result = Backbone.Model.prototype.unset.apply( this, arguments ); |
---|
1302 | this.updateRelations( options ); |
---|
1303 | |
---|
1304 | // Try to run the global queue holding external events |
---|
1305 | Backbone.Relational.eventQueue.unblock(); |
---|
1306 | |
---|
1307 | return result; |
---|
1308 | }, |
---|
1309 | |
---|
1310 | clear: function( options ) { |
---|
1311 | Backbone.Relational.eventQueue.block(); |
---|
1312 | |
---|
1313 | var result = Backbone.Model.prototype.clear.apply( this, arguments ); |
---|
1314 | this.updateRelations( options ); |
---|
1315 | |
---|
1316 | // Try to run the global queue holding external events |
---|
1317 | Backbone.Relational.eventQueue.unblock(); |
---|
1318 | |
---|
1319 | return result; |
---|
1320 | }, |
---|
1321 | |
---|
1322 | clone: function() { |
---|
1323 | var attributes = _.clone( this.attributes ); |
---|
1324 | if ( !_.isUndefined( attributes[ this.idAttribute ] ) ) { |
---|
1325 | attributes[ this.idAttribute ] = null; |
---|
1326 | } |
---|
1327 | |
---|
1328 | _.each( this.getRelations() || [], function( rel ) { |
---|
1329 | delete attributes[ rel.key ]; |
---|
1330 | }); |
---|
1331 | |
---|
1332 | return new this.constructor( attributes ); |
---|
1333 | }, |
---|
1334 | |
---|
1335 | /** |
---|
1336 | * Convert relations to JSON, omits them when required |
---|
1337 | */ |
---|
1338 | toJSON: function(options) { |
---|
1339 | // If this Model has already been fully serialized in this branch once, return to avoid loops |
---|
1340 | if ( this.isLocked() ) { |
---|
1341 | return this.id; |
---|
1342 | } |
---|
1343 | |
---|
1344 | this.acquire(); |
---|
1345 | var json = Backbone.Model.prototype.toJSON.call( this, options ); |
---|
1346 | |
---|
1347 | if ( this.constructor._superModel && !( this.constructor._subModelTypeAttribute in json ) ) { |
---|
1348 | json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue; |
---|
1349 | } |
---|
1350 | |
---|
1351 | _.each( this._relations || [], function( rel ) { |
---|
1352 | var value = json[ rel.key ]; |
---|
1353 | |
---|
1354 | if ( rel.options.includeInJSON === true) { |
---|
1355 | if ( value && _.isFunction( value.toJSON ) ) { |
---|
1356 | json[ rel.keyDestination ] = value.toJSON( options ); |
---|
1357 | } |
---|
1358 | else { |
---|
1359 | json[ rel.keyDestination ] = null; |
---|
1360 | } |
---|
1361 | } |
---|
1362 | else if ( _.isString( rel.options.includeInJSON ) ) { |
---|
1363 | if ( value instanceof Backbone.Collection ) { |
---|
1364 | json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON ); |
---|
1365 | } |
---|
1366 | else if ( value instanceof Backbone.Model ) { |
---|
1367 | json[ rel.keyDestination ] = value.get( rel.options.includeInJSON ); |
---|
1368 | } |
---|
1369 | else { |
---|
1370 | json[ rel.keyDestination ] = null; |
---|
1371 | } |
---|
1372 | } |
---|
1373 | else if ( _.isArray( rel.options.includeInJSON ) ) { |
---|
1374 | if ( value instanceof Backbone.Collection ) { |
---|
1375 | var valueSub = []; |
---|
1376 | value.each( function( model ) { |
---|
1377 | var curJson = {}; |
---|
1378 | _.each( rel.options.includeInJSON, function( key ) { |
---|
1379 | curJson[ key ] = model.get( key ); |
---|
1380 | }); |
---|
1381 | valueSub.push( curJson ); |
---|
1382 | }); |
---|
1383 | json[ rel.keyDestination ] = valueSub; |
---|
1384 | } |
---|
1385 | else if ( value instanceof Backbone.Model ) { |
---|
1386 | var valueSub = {}; |
---|
1387 | _.each( rel.options.includeInJSON, function( key ) { |
---|
1388 | valueSub[ key ] = value.get( key ); |
---|
1389 | }); |
---|
1390 | json[ rel.keyDestination ] = valueSub; |
---|
1391 | } |
---|
1392 | else { |
---|
1393 | json[ rel.keyDestination ] = null; |
---|
1394 | } |
---|
1395 | } |
---|
1396 | else { |
---|
1397 | delete json[ rel.key ]; |
---|
1398 | } |
---|
1399 | |
---|
1400 | if ( rel.keyDestination !== rel.key ) { |
---|
1401 | delete json[ rel.key ]; |
---|
1402 | } |
---|
1403 | }); |
---|
1404 | |
---|
1405 | this.release(); |
---|
1406 | return json; |
---|
1407 | } |
---|
1408 | }, |
---|
1409 | { |
---|
1410 | setup: function( superModel ) { |
---|
1411 | // We don't want to share a relations array with a parent, as this will cause problems with |
---|
1412 | // reverse relations. |
---|
1413 | this.prototype.relations = ( this.prototype.relations || [] ).slice( 0 ); |
---|
1414 | |
---|
1415 | this._subModels = {}; |
---|
1416 | this._superModel = null; |
---|
1417 | |
---|
1418 | // If this model has 'subModelTypes' itself, remember them in the store |
---|
1419 | if ( this.prototype.hasOwnProperty( 'subModelTypes' ) ) { |
---|
1420 | Backbone.Relational.store.addSubModels( this.prototype.subModelTypes, this ); |
---|
1421 | } |
---|
1422 | // The 'subModelTypes' property should not be inherited, so reset it. |
---|
1423 | else { |
---|
1424 | this.prototype.subModelTypes = null; |
---|
1425 | } |
---|
1426 | |
---|
1427 | // Initialize all reverseRelations that belong to this new model. |
---|
1428 | _.each( this.prototype.relations || [], function( rel ) { |
---|
1429 | if ( !rel.model ) { |
---|
1430 | rel.model = this; |
---|
1431 | } |
---|
1432 | |
---|
1433 | if ( rel.reverseRelation && rel.model === this ) { |
---|
1434 | var preInitialize = true; |
---|
1435 | if ( _.isString( rel.relatedModel ) ) { |
---|
1436 | /** |
---|
1437 | * The related model might not be defined for two reasons |
---|
1438 | * 1. it never gets defined, e.g. a typo |
---|
1439 | * 2. it is related to itself |
---|
1440 | * In neither of these cases do we need to pre-initialize reverse relations. |
---|
1441 | */ |
---|
1442 | var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel ); |
---|
1443 | preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel ); |
---|
1444 | } |
---|
1445 | |
---|
1446 | var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type ); |
---|
1447 | if ( preInitialize && type && type.prototype instanceof Backbone.Relation ) { |
---|
1448 | new type( null, rel ); |
---|
1449 | } |
---|
1450 | } |
---|
1451 | }, this ); |
---|
1452 | |
---|
1453 | return this; |
---|
1454 | }, |
---|
1455 | |
---|
1456 | /** |
---|
1457 | * Create a 'Backbone.Model' instance based on 'attributes'. |
---|
1458 | * @param {Object} attributes |
---|
1459 | * @param {Object} [options] |
---|
1460 | * @return {Backbone.Model} |
---|
1461 | */ |
---|
1462 | build: function( attributes, options ) { |
---|
1463 | var model = this; |
---|
1464 | |
---|
1465 | // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet. |
---|
1466 | this.initializeModelHierarchy(); |
---|
1467 | |
---|
1468 | // Determine what type of (sub)model should be built if applicable. |
---|
1469 | // Lookup the proper subModelType in 'this._subModels'. |
---|
1470 | if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) { |
---|
1471 | var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ]; |
---|
1472 | var subModelType = this._subModels[ subModelTypeAttribute ]; |
---|
1473 | if ( subModelType ) { |
---|
1474 | model = subModelType; |
---|
1475 | } |
---|
1476 | } |
---|
1477 | |
---|
1478 | return new model( attributes, options ); |
---|
1479 | }, |
---|
1480 | |
---|
1481 | initializeModelHierarchy: function() { |
---|
1482 | // If we're here for the first time, try to determine if this modelType has a 'superModel'. |
---|
1483 | if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) { |
---|
1484 | Backbone.Relational.store.setupSuperModel( this ); |
---|
1485 | |
---|
1486 | // If a superModel has been found, copy relations from the _superModel if they haven't been |
---|
1487 | // inherited automatically (due to a redefinition of 'relations'). |
---|
1488 | // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail |
---|
1489 | // the isUndefined/isNull check next time. |
---|
1490 | if ( this._superModel ) { |
---|
1491 | // |
---|
1492 | if ( this._superModel.prototype.relations ) { |
---|
1493 | var supermodelRelationsExist = _.any( this.prototype.relations || [], function( rel ) { |
---|
1494 | return rel.model && rel.model !== this; |
---|
1495 | }, this ); |
---|
1496 | |
---|
1497 | if ( !supermodelRelationsExist ) { |
---|
1498 | this.prototype.relations = this._superModel.prototype.relations.concat( this.prototype.relations ); |
---|
1499 | } |
---|
1500 | } |
---|
1501 | } |
---|
1502 | else { |
---|
1503 | this._superModel = false; |
---|
1504 | } |
---|
1505 | } |
---|
1506 | |
---|
1507 | // If we came here through 'build' for a model that has 'subModelTypes', and not all of them have been resolved yet, try to resolve each. |
---|
1508 | if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) { |
---|
1509 | _.each( this.prototype.subModelTypes || [], function( subModelTypeName ) { |
---|
1510 | var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName ); |
---|
1511 | subModelType && subModelType.initializeModelHierarchy(); |
---|
1512 | }); |
---|
1513 | } |
---|
1514 | }, |
---|
1515 | |
---|
1516 | /** |
---|
1517 | * Find an instance of `this` type in 'Backbone.Relational.store'. |
---|
1518 | * - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found. |
---|
1519 | * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`. |
---|
1520 | * Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`). |
---|
1521 | * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model. |
---|
1522 | * @param {Object} [options] |
---|
1523 | * @param {Boolean} [options.create=true] |
---|
1524 | * @param {Boolean} [options.update=true] |
---|
1525 | * @return {Backbone.RelationalModel} |
---|
1526 | */ |
---|
1527 | findOrCreate: function( attributes, options ) { |
---|
1528 | options || ( options = {} ); |
---|
1529 | var parsedAttributes = (_.isObject( attributes ) && this.prototype.parse) ? this.prototype.parse( attributes ) : attributes; |
---|
1530 | // Try to find an instance of 'this' model type in the store |
---|
1531 | var model = Backbone.Relational.store.find( this, parsedAttributes ); |
---|
1532 | |
---|
1533 | // If we found an instance, update it with the data in 'item' (unless 'options.update' is false). |
---|
1534 | // If not, create an instance (unless 'options.create' is false). |
---|
1535 | if ( _.isObject( attributes ) ) { |
---|
1536 | if ( model && options.update !== false ) { |
---|
1537 | model.set( parsedAttributes, options ); |
---|
1538 | } |
---|
1539 | else if ( !model && options.create !== false ) { |
---|
1540 | model = this.build( attributes, options ); |
---|
1541 | } |
---|
1542 | } |
---|
1543 | |
---|
1544 | return model; |
---|
1545 | } |
---|
1546 | }); |
---|
1547 | _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore ); |
---|
1548 | |
---|
1549 | /** |
---|
1550 | * Override Backbone.Collection._prepareModel, so objects will be built using the correct type |
---|
1551 | * if the collection.model has subModels. |
---|
1552 | */ |
---|
1553 | Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel; |
---|
1554 | Backbone.Collection.prototype._prepareModel = function ( attrs, options ) { |
---|
1555 | var model; |
---|
1556 | |
---|
1557 | if ( attrs instanceof Backbone.Model ) { |
---|
1558 | if ( !attrs.collection ) { |
---|
1559 | attrs.collection = this; |
---|
1560 | } |
---|
1561 | model = attrs; |
---|
1562 | } |
---|
1563 | else { |
---|
1564 | options || (options = {}); |
---|
1565 | options.collection = this; |
---|
1566 | |
---|
1567 | if ( typeof this.model.findOrCreate !== 'undefined' ) { |
---|
1568 | model = this.model.findOrCreate( attrs, options ); |
---|
1569 | } |
---|
1570 | else { |
---|
1571 | model = new this.model( attrs, options ); |
---|
1572 | } |
---|
1573 | |
---|
1574 | if ( !model._validate( attrs, options ) ) { |
---|
1575 | model = false; |
---|
1576 | } |
---|
1577 | } |
---|
1578 | |
---|
1579 | return model; |
---|
1580 | }; |
---|
1581 | |
---|
1582 | |
---|
1583 | /** |
---|
1584 | * Override Backbone.Collection.add, so objects fetched from the server multiple times will |
---|
1585 | * update the existing Model. Also, trigger 'relational:add'. |
---|
1586 | */ |
---|
1587 | var add = Backbone.Collection.prototype.__add = Backbone.Collection.prototype.add; |
---|
1588 | Backbone.Collection.prototype.add = function( models, options ) { |
---|
1589 | options || (options = {}); |
---|
1590 | if ( !_.isArray( models ) ) { |
---|
1591 | models = [ models ]; |
---|
1592 | } |
---|
1593 | |
---|
1594 | var modelsToAdd = []; |
---|
1595 | |
---|
1596 | //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options ); |
---|
1597 | _.each( models || [], function( model ) { |
---|
1598 | if ( !( model instanceof Backbone.Model ) ) { |
---|
1599 | // `_prepareModel` attempts to find `model` in Backbone.store through `findOrCreate`, |
---|
1600 | // and sets the new properties on it if is found. Otherwise, a new model is instantiated. |
---|
1601 | model = Backbone.Collection.prototype._prepareModel.call( this, model, options ); |
---|
1602 | } |
---|
1603 | |
---|
1604 | if ( model instanceof Backbone.Model && !this.get( model ) ) { |
---|
1605 | modelsToAdd.push( model ); |
---|
1606 | } |
---|
1607 | }, this ); |
---|
1608 | |
---|
1609 | // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc). |
---|
1610 | if ( modelsToAdd.length ) { |
---|
1611 | add.call( this, modelsToAdd, options ); |
---|
1612 | |
---|
1613 | _.each( modelsToAdd || [], function( model ) { |
---|
1614 | this.trigger( 'relational:add', model, this, options ); |
---|
1615 | }, this ); |
---|
1616 | } |
---|
1617 | |
---|
1618 | return this; |
---|
1619 | }; |
---|
1620 | |
---|
1621 | /** |
---|
1622 | * Override 'Backbone.Collection.remove' to trigger 'relational:remove'. |
---|
1623 | */ |
---|
1624 | var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove; |
---|
1625 | Backbone.Collection.prototype.remove = function( models, options ) { |
---|
1626 | options || (options = {}); |
---|
1627 | if ( !_.isArray( models ) ) { |
---|
1628 | models = [ models ]; |
---|
1629 | } |
---|
1630 | else { |
---|
1631 | models = models.slice( 0 ); |
---|
1632 | } |
---|
1633 | |
---|
1634 | //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options ); |
---|
1635 | _.each( models || [], function( model ) { |
---|
1636 | model = this.get( model ); |
---|
1637 | |
---|
1638 | if ( model instanceof Backbone.Model ) { |
---|
1639 | remove.call( this, model, options ); |
---|
1640 | this.trigger('relational:remove', model, this, options); |
---|
1641 | } |
---|
1642 | }, this ); |
---|
1643 | |
---|
1644 | return this; |
---|
1645 | }; |
---|
1646 | |
---|
1647 | /** |
---|
1648 | * Override 'Backbone.Collection.reset' to trigger 'relational:reset'. |
---|
1649 | */ |
---|
1650 | var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset; |
---|
1651 | Backbone.Collection.prototype.reset = function( models, options ) { |
---|
1652 | reset.call( this, models, options ); |
---|
1653 | this.trigger( 'relational:reset', this, options ); |
---|
1654 | |
---|
1655 | return this; |
---|
1656 | }; |
---|
1657 | |
---|
1658 | /** |
---|
1659 | * Override 'Backbone.Collection.sort' to trigger 'relational:reset'. |
---|
1660 | */ |
---|
1661 | var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort; |
---|
1662 | Backbone.Collection.prototype.sort = function( options ) { |
---|
1663 | sort.call( this, options ); |
---|
1664 | this.trigger( 'relational:reset', this, options ); |
---|
1665 | |
---|
1666 | return this; |
---|
1667 | }; |
---|
1668 | |
---|
1669 | /** |
---|
1670 | * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations |
---|
1671 | * are ready. |
---|
1672 | */ |
---|
1673 | var trigger = Backbone.Collection.prototype.__trigger = Backbone.Collection.prototype.trigger; |
---|
1674 | Backbone.Collection.prototype.trigger = function( eventName ) { |
---|
1675 | if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) { |
---|
1676 | var dit = this, args = arguments; |
---|
1677 | |
---|
1678 | if (eventName === 'add') { |
---|
1679 | args = _.toArray( args ); |
---|
1680 | // the fourth argument in case of a regular add is the option object. |
---|
1681 | // we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked |
---|
1682 | if (_.isObject( args[3] ) ) { |
---|
1683 | args[3] = _.clone( args[3] ); |
---|
1684 | } |
---|
1685 | } |
---|
1686 | |
---|
1687 | Backbone.Relational.eventQueue.add( function() { |
---|
1688 | trigger.apply( dit, args ); |
---|
1689 | }); |
---|
1690 | } |
---|
1691 | else { |
---|
1692 | trigger.apply( this, arguments ); |
---|
1693 | } |
---|
1694 | |
---|
1695 | return this; |
---|
1696 | }; |
---|
1697 | |
---|
1698 | // Override .extend() to automatically call .setup() |
---|
1699 | Backbone.RelationalModel.extend = function( protoProps, classProps ) { |
---|
1700 | var child = Backbone.Model.extend.apply( this, arguments ); |
---|
1701 | |
---|
1702 | child.setup( this ); |
---|
1703 | |
---|
1704 | return child; |
---|
1705 | }; |
---|
1706 | })(); |
---|