1 /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */ |
1 /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */ |
2 /** |
2 /** |
3 * Backbone-relational.js 0.8.5 |
3 * Backbone-relational.js 0.10.0 |
4 * (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors) |
4 * (c) 2011-2014 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors) |
5 * |
5 * |
6 * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt. |
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. |
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. |
8 * Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone. |
|
9 * |
|
10 * Example: |
|
11 * |
|
12 Zoo = Backbone.RelationalModel.extend({ |
|
13 relations: [ { |
|
14 type: Backbone.HasMany, |
|
15 key: 'animals', |
|
16 relatedModel: 'Animal', |
|
17 reverseRelation: { |
|
18 key: 'livesIn', |
|
19 includeInJSON: 'id' |
|
20 // 'relatedModel' is automatically set to 'Zoo'; the 'relationType' to 'HasOne'. |
|
21 } |
|
22 } ], |
|
23 |
|
24 toString: function() { |
|
25 return this.get( 'name' ); |
|
26 } |
|
27 }); |
|
28 |
|
29 Animal = Backbone.RelationalModel.extend({ |
|
30 toString: function() { |
|
31 return this.get( 'species' ); |
|
32 } |
|
33 }); |
|
34 |
|
35 // Creating the zoo will give it a collection with one animal in it: the monkey. |
|
36 // The animal created after that has a relation `livesIn` that points to the zoo it's currently associated with. |
|
37 // If you instantiate (or fetch) the zebra later, it will automatically be added. |
|
38 |
|
39 var zoo = new Zoo({ |
|
40 name: 'Artis', |
|
41 animals: [ { id: 'monkey-1', species: 'Chimp' }, 'lion-1', 'zebra-1' ] |
|
42 }); |
|
43 |
|
44 var lion = new Animal( { id: 'lion-1', species: 'Lion' } ), |
|
45 monkey = zoo.get( 'animals' ).first(), |
|
46 sameZoo = lion.get( 'livesIn' ); |
9 */ |
47 */ |
10 ( function( undefined ) { |
48 ( function( root, factory ) { |
|
49 // Set up Backbone-relational for the environment. Start with AMD. |
|
50 if ( typeof define === 'function' && define.amd ) { |
|
51 define( [ 'exports', 'backbone', 'underscore' ], factory ); |
|
52 } |
|
53 // Next for Node.js or CommonJS. |
|
54 else if ( typeof exports !== 'undefined' ) { |
|
55 factory( exports, require( 'backbone' ), require( 'underscore' ) ); |
|
56 } |
|
57 // Finally, as a browser global. Use `root` here as it references `window`. |
|
58 else { |
|
59 factory( root, root.Backbone, root._ ); |
|
60 } |
|
61 }( this, function( exports, Backbone, _ ) { |
11 "use strict"; |
62 "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 |
63 |
28 Backbone.Relational = { |
64 Backbone.Relational = { |
29 showWarnings: true |
65 showWarnings: true |
30 }; |
66 }; |
31 |
67 |
433 * Explicitly update a model's id in its store collection |
482 * Explicitly update a model's id in its store collection |
434 * @param {Backbone.RelationalModel} model |
483 * @param {Backbone.RelationalModel} model |
435 */ |
484 */ |
436 update: function( model ) { |
485 update: function( model ) { |
437 var coll = this.getCollection( model ); |
486 var coll = this.getCollection( model ); |
|
487 |
|
488 // Register a model if it isn't yet (which happens if it was created without an id). |
|
489 if ( !coll.contains( model ) ) { |
|
490 this.register( model ); |
|
491 } |
|
492 |
438 // This triggers updating the lookup indices kept in a collection |
493 // This triggers updating the lookup indices kept in a collection |
439 coll._onModelEvent( 'change:' + model.idAttribute, model, coll ); |
494 coll._onModelEvent( 'change:' + model.idAttribute, model, coll ); |
440 |
495 |
441 // Trigger an event on model so related models (having the model's new id in their keyContents) can add it. |
496 // Trigger an event on model so related models (having the model's new id in their keyContents) can add it. |
442 model.trigger( 'relational:change:id', model, coll ); |
497 model.trigger( 'relational:change:id', model, coll ); |
443 }, |
498 }, |
444 |
499 |
445 /** |
500 /** |
446 * Remove a 'model' from the store. |
501 * Unregister from the store: a specific model, a collection, or a model type. |
447 * @param {Backbone.RelationalModel} model |
502 * @param {Backbone.RelationalModel|Backbone.RelationalModel.constructor|Backbone.Collection} type |
448 */ |
503 */ |
449 unregister: function( model ) { |
504 unregister: function( type ) { |
450 this.stopListening( model, 'destroy', this.unregister ); |
505 var coll, |
451 var coll = this.getCollection( model ); |
506 models; |
452 coll && coll.remove( model ); |
507 |
|
508 if ( type instanceof Backbone.Model ) { |
|
509 coll = this.getCollection( type ); |
|
510 models = [ type ]; |
|
511 } |
|
512 else if ( type instanceof Backbone.Collection ) { |
|
513 coll = this.getCollection( type.model ); |
|
514 models = _.clone( type.models ); |
|
515 } |
|
516 else { |
|
517 coll = this.getCollection( type ); |
|
518 models = _.clone( coll.models ); |
|
519 } |
|
520 |
|
521 _.each( models, function( model ) { |
|
522 this.stopListening( model ); |
|
523 _.invoke( model.getRelations(), 'stopListening' ); |
|
524 }, this ); |
|
525 |
|
526 |
|
527 // If we've unregistered an entire store collection, reset the collection (which is much faster). |
|
528 // Otherwise, remove each model one by one. |
|
529 if ( _.contains( this._collections, type ) ) { |
|
530 coll.reset( [] ); |
|
531 } |
|
532 else { |
|
533 _.each( models, function( model ) { |
|
534 if ( coll.get( model ) ) { |
|
535 coll.remove( model ); |
|
536 } |
|
537 else { |
|
538 coll.trigger( 'relational:remove', model, coll ); |
|
539 } |
|
540 }, this ); |
|
541 } |
453 }, |
542 }, |
454 |
543 |
455 /** |
544 /** |
456 * Reset the `store` to it's original state. The `reverseRelations` are kept though, since attempting to |
545 * Reset the `store` to it's original state. The `reverseRelations` are kept though, since attempting to |
457 * re-initialize these on models would lead to a large amount of warnings. |
546 * re-initialize these on models would lead to a large amount of warnings. |
458 */ |
547 */ |
459 reset: function() { |
548 reset: function() { |
460 this.stopListening(); |
549 this.stopListening(); |
|
550 |
|
551 // Unregister each collection to remove event listeners |
|
552 _.each( this._collections, function( coll ) { |
|
553 this.unregister( coll ); |
|
554 }, this ); |
|
555 |
461 this._collections = []; |
556 this._collections = []; |
462 this._subModels = []; |
557 this._subModels = []; |
463 this._modelScopes = [ exports ]; |
558 this._modelScopes = [ exports ]; |
464 } |
559 } |
465 }); |
560 }); |
517 ) ); |
624 ) ); |
518 } |
625 } |
519 |
626 |
520 if ( instance ) { |
627 if ( instance ) { |
521 var contentKey = this.keySource; |
628 var contentKey = this.keySource; |
522 if ( contentKey !== this.key && typeof this.instance.get( this.key ) === 'object' ) { |
629 if ( contentKey !== this.key && _.isObject( this.instance.get( this.key ) ) ) { |
523 contentKey = this.key; |
630 contentKey = this.key; |
524 } |
631 } |
525 |
632 |
526 this.setKeyContents( this.instance.get( contentKey ) ); |
633 this.setKeyContents( this.instance.get( contentKey ) ); |
527 this.relatedCollection = Backbone.Relational.store.getCollection( this.relatedModel ); |
634 this.relatedCollection = Backbone.Relational.store.getCollection( this.relatedModel ); |
528 |
635 |
529 // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'. |
636 // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'. |
530 if ( this.keySource !== this.key ) { |
637 if ( this.keySource !== this.key ) { |
531 this.instance.unset( this.keySource, { silent: true } ); |
638 delete this.instance.attributes[ this.keySource ]; |
532 } |
639 } |
533 |
640 |
534 // Add this Relation to instance._relations |
641 // Add this Relation to instance._relations |
535 this.instance._relations[ this.key ] = this; |
642 this.instance._relations[ this.key ] = this; |
536 |
643 |
537 this.initialize( opts ); |
644 this.initialize( opts ); |
538 |
645 |
539 if ( this.options.autoFetch ) { |
646 if ( this.options.autoFetch ) { |
540 this.instance.fetchRelated( this.key, _.isObject( this.options.autoFetch ) ? this.options.autoFetch : {} ); |
647 this.instance.getAsync( this.key, _.isObject( this.options.autoFetch ) ? this.options.autoFetch : {} ); |
541 } |
648 } |
542 |
649 |
543 // When 'relatedModel' are created or destroyed, check if it affects this relation. |
650 // When 'relatedModel' are created or destroyed, check if it affects this relation. |
544 this.listenTo( this.instance, 'destroy', this.destroy ) |
651 this.listenTo( this.instance, 'destroy', this.destroy ) |
545 .listenTo( this.relatedCollection, 'relational:add relational:change:id', this.tryAddRelated ) |
652 .listenTo( this.relatedCollection, 'relational:add relational:change:id', this.tryAddRelated ) |
546 .listenTo( this.relatedCollection, 'relational:remove', this.removeRelated ) |
653 .listenTo( this.relatedCollection, 'relational:remove', this.removeRelated ); |
547 } |
654 } |
548 }; |
655 }; |
549 // Fix inheritance :\ |
656 // Fix inheritance :\ |
550 Backbone.Relation.extend = Backbone.Model.extend; |
657 Backbone.Relation.extend = Backbone.Model.extend; |
551 // Set up all inheritable **Backbone.Relation** properties and methods. |
658 // Set up all inheritable **Backbone.Relation** properties and methods. |
1118 trigger: function( eventName ) { |
1235 trigger: function( eventName ) { |
1119 if ( eventName.length > 5 && eventName.indexOf( 'change' ) === 0 ) { |
1236 if ( eventName.length > 5 && eventName.indexOf( 'change' ) === 0 ) { |
1120 var dit = this, |
1237 var dit = this, |
1121 args = arguments; |
1238 args = arguments; |
1122 |
1239 |
1123 Backbone.Relational.eventQueue.add( function() { |
1240 if ( !Backbone.Relational.eventQueue.isBlocked() ) { |
1124 if ( !dit._isInitialized ) { |
1241 // If we're not in a more complicated nested scenario, fire the change event right away |
1125 return; |
1242 Backbone.Model.prototype.trigger.apply( dit, args ); |
1126 } |
1243 } |
1127 |
1244 else { |
1128 // Determine if the `change` event is still valid, now that all relations are populated |
1245 Backbone.Relational.eventQueue.add( function() { |
1129 var changed = true; |
1246 // Determine if the `change` event is still valid, now that all relations are populated |
1130 if ( eventName === 'change' ) { |
1247 var changed = true; |
1131 changed = dit.hasChanged(); |
1248 if ( eventName === 'change' ) { |
1132 } |
1249 // `hasChanged` may have gotten reset by nested calls to `set`. |
1133 else { |
1250 changed = dit.hasChanged() || dit._attributeChangeFired; |
1134 var attr = eventName.slice( 7 ), |
1251 dit._attributeChangeFired = false; |
1135 rel = dit.getRelation( attr ); |
1252 } |
1136 |
1253 else { |
1137 if ( rel ) { |
1254 var attr = eventName.slice( 7 ), |
1138 // If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`. |
1255 rel = dit.getRelation( attr ); |
1139 // These take precedence over `change:attr` events triggered by `Model.set`. |
1256 |
1140 // The relation set a fourth attribute to `true`. If this attribute is present, |
1257 if ( rel ) { |
1141 // continue triggering this event; otherwise, it's from `Model.set` and should be stopped. |
1258 // If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`. |
1142 changed = ( args[ 4 ] === true ); |
1259 // These take precedence over `change:attr` events triggered by `Model.set`. |
1143 |
1260 // The relation sets a fourth attribute to `true`. If this attribute is present, |
1144 // If this event was triggered by a relation, set the right value in `this.changed` |
1261 // continue triggering this event; otherwise, it's from `Model.set` and should be stopped. |
1145 // (a Collection or Model instead of raw data). |
1262 changed = ( args[ 4 ] === true ); |
1146 if ( changed ) { |
1263 |
1147 dit.changed[ attr ] = args[ 2 ]; |
1264 // If this event was triggered by a relation, set the right value in `this.changed` |
|
1265 // (a Collection or Model instead of raw data). |
|
1266 if ( changed ) { |
|
1267 dit.changed[ attr ] = args[ 2 ]; |
|
1268 } |
|
1269 // Otherwise, this event is from `Model.set`. If the relation doesn't report a change, |
|
1270 // remove attr from `dit.changed` so `hasChanged` doesn't take it into account. |
|
1271 else if ( !rel.changed ) { |
|
1272 delete dit.changed[ attr ]; |
|
1273 } |
1148 } |
1274 } |
1149 // Otherwise, this event is from `Model.set`. If the relation doesn't report a change, |
1275 else if ( changed ) { |
1150 // remove attr from `dit.changed` so `hasChanged` doesn't take it into account. |
1276 dit._attributeChangeFired = true; |
1151 else if ( !rel.changed ) { |
|
1152 delete dit.changed[ attr ]; |
|
1153 } |
1277 } |
1154 } |
1278 } |
1155 } |
1279 |
1156 |
1280 changed && Backbone.Model.prototype.trigger.apply( dit, args ); |
1157 changed && Backbone.Model.prototype.trigger.apply( dit, args ); |
1281 }); |
1158 }); |
1282 } |
|
1283 } |
|
1284 else if ( eventName === 'destroy' ) { |
|
1285 Backbone.Model.prototype.trigger.apply( this, arguments ); |
|
1286 Backbone.Relational.store.unregister( this ); |
1159 } |
1287 } |
1160 else { |
1288 else { |
1161 Backbone.Model.prototype.trigger.apply( this, arguments ); |
1289 Backbone.Model.prototype.trigger.apply( this, arguments ); |
1162 } |
1290 } |
1163 |
1291 |
1213 } |
1354 } |
1214 }, |
1355 }, |
1215 |
1356 |
1216 /** |
1357 /** |
1217 * Get a specific relation. |
1358 * Get a specific relation. |
1218 * @param key {string} The relation key to look for. |
1359 * @param {string} attr The relation key to look for. |
1219 * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'key', or null. |
1360 * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'attr', or null. |
1220 */ |
1361 */ |
1221 getRelation: function( key ) { |
1362 getRelation: function( attr ) { |
1222 return this._relations[ key ]; |
1363 return this._relations[ attr ]; |
1223 }, |
1364 }, |
1224 |
1365 |
1225 /** |
1366 /** |
1226 * Get all of the created relations. |
1367 * Get all of the created relations. |
1227 * @return {Backbone.Relation[]} |
1368 * @return {Backbone.Relation[]} |
1228 */ |
1369 */ |
1229 getRelations: function() { |
1370 getRelations: function() { |
1230 return _.values( this._relations ); |
1371 return _.values( this._relations ); |
1231 }, |
1372 }, |
1232 |
1373 |
1233 /** |
1374 |
1234 * Retrieve related objects. |
1375 /** |
1235 * @param key {string} The relation key to fetch models for. |
1376 * Get a list of ids that will be fetched on a call to `getAsync`. |
1236 * @param [options] {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'. |
1377 * @param {string|Backbone.Relation} attr The relation key to fetch models for. |
1237 * @param [refresh=false] {boolean} Fetch existing models from the server as well (in order to update them). |
1378 * @param [refresh=false] Add ids for models that are already in the relation, refreshing them? |
1238 * @return {jQuery.when[]} An array of request objects |
1379 * @return {Array} An array of ids that need to be fetched. |
1239 */ |
1380 */ |
1240 fetchRelated: function( key, options, refresh ) { |
1381 getIdsToFetch: function( attr, refresh ) { |
1241 // Set default `options` for fetch |
1382 var rel = attr instanceof Backbone.Relation ? attr : this.getRelation( attr ), |
1242 options = _.extend( { update: true, remove: false }, options ); |
1383 ids = rel ? ( rel.keyIds && rel.keyIds.slice( 0 ) ) || ( ( rel.keyId || rel.keyId === 0 ) ? [ rel.keyId ] : [] ) : []; |
1243 |
|
1244 var setUrl, |
|
1245 requests = [], |
|
1246 rel = this.getRelation( key ), |
|
1247 idsToFetch = rel && ( rel.keyIds || ( ( rel.keyId || rel.keyId === 0 ) ? [ rel.keyId ] : [] ) ); |
|
1248 |
1384 |
1249 // On `refresh`, add the ids for current models in the relation to `idsToFetch` |
1385 // On `refresh`, add the ids for current models in the relation to `idsToFetch` |
1250 if ( refresh ) { |
1386 if ( refresh ) { |
1251 var models = rel.related instanceof Backbone.Collection ? rel.related.models : [ rel.related ]; |
1387 var models = rel.related && ( rel.related.models || [ rel.related ] ); |
1252 _.each( models, function( model ) { |
1388 _.each( models, function( model ) { |
1253 if ( model.id || model.id === 0 ) { |
1389 if ( model.id || model.id === 0 ) { |
1254 idsToFetch.push( model.id ); |
1390 ids.push( model.id ); |
1255 } |
1391 } |
1256 }); |
1392 }); |
1257 } |
1393 } |
1258 |
1394 |
|
1395 return ids; |
|
1396 }, |
|
1397 |
|
1398 /** |
|
1399 * Get related objects. Returns a single promise, which can either resolve immediately (if the related model[s]) |
|
1400 * are already present locally, or after fetching the contents of the requested attribute. |
|
1401 * @param {string} attr The relation key to fetch models for. |
|
1402 * @param {Object} [options] Options for 'Backbone.Model.fetch' and 'Backbone.sync'. |
|
1403 * @param {Boolean} [options.refresh=false] Fetch existing models from the server as well (in order to update them). |
|
1404 * @return {jQuery.Deferred} A jQuery promise object. When resolved, its `done` callback will be called with |
|
1405 * contents of `attr`. |
|
1406 */ |
|
1407 getAsync: function( attr, options ) { |
|
1408 // Set default `options` for fetch |
|
1409 options = _.extend( { add: true, remove: false, refresh: false }, options ); |
|
1410 |
|
1411 var dit = this, |
|
1412 requests = [], |
|
1413 rel = this.getRelation( attr ), |
|
1414 idsToFetch = rel && this.getIdsToFetch( rel, options.refresh ), |
|
1415 coll = rel.related instanceof Backbone.Collection ? rel.related : rel.relatedCollection; |
|
1416 |
1259 if ( idsToFetch && idsToFetch.length ) { |
1417 if ( idsToFetch && idsToFetch.length ) { |
1260 // Find (or create) a model for each one that is to be fetched |
1418 var models = [], |
1261 var created = [], |
1419 createdModels = [], |
1262 models = _.map( idsToFetch, function( id ) { |
1420 setUrl, |
1263 var model = Backbone.Relational.store.find( rel.relatedModel, id ); |
1421 createModels = function() { |
1264 |
1422 // Find (or create) a model for each one that is to be fetched |
1265 if ( !model ) { |
1423 models = _.map( idsToFetch, function( id ) { |
1266 var attrs = {}; |
1424 var model = rel.relatedModel.findModel( id ); |
1267 attrs[ rel.relatedModel.prototype.idAttribute ] = id; |
1425 |
1268 model = rel.relatedModel.findOrCreate( attrs, options ); |
1426 if ( !model ) { |
1269 created.push( model ); |
1427 var attrs = {}; |
1270 } |
1428 attrs[ rel.relatedModel.prototype.idAttribute ] = id; |
1271 |
1429 model = rel.relatedModel.findOrCreate( attrs, options ); |
1272 return model; |
1430 createdModels.push( model ); |
1273 }, this ); |
1431 } |
1274 |
1432 |
|
1433 return model; |
|
1434 }, this ); |
|
1435 }; |
|
1436 |
1275 // Try if the 'collection' can provide a url to fetch a set of models in one request. |
1437 // Try if the 'collection' can provide a url to fetch a set of models in one request. |
1276 if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) { |
1438 // This assumes that when 'Backbone.Collection.url' is a function, it can handle building of set urls. |
1277 setUrl = rel.related.url( models ); |
|
1278 } |
|
1279 |
|
1280 // An assumption is that when 'Backbone.Collection.url' is a function, it can handle building of set urls. |
|
1281 // To make sure it can, test if the url we got by supplying a list of models to fetch is different from |
1439 // To make sure it can, test if the url we got by supplying a list of models to fetch is different from |
1282 // the one supplied for the default fetch action (without args to 'url'). |
1440 // the one supplied for the default fetch action (without args to 'url'). |
1283 if ( setUrl && setUrl !== rel.related.url() ) { |
1441 if ( coll instanceof Backbone.Collection && _.isFunction( coll.url ) ) { |
|
1442 var defaultUrl = coll.url(); |
|
1443 setUrl = coll.url( idsToFetch ); |
|
1444 |
|
1445 if ( setUrl === defaultUrl ) { |
|
1446 createModels(); |
|
1447 setUrl = coll.url( models ); |
|
1448 |
|
1449 if ( setUrl === defaultUrl ) { |
|
1450 setUrl = null; |
|
1451 } |
|
1452 } |
|
1453 } |
|
1454 |
|
1455 if ( setUrl ) { |
|
1456 // Do a single request to fetch all models |
1284 var opts = _.defaults( |
1457 var opts = _.defaults( |
1285 { |
1458 { |
1286 error: function() { |
1459 error: function() { |
1287 var args = arguments; |
1460 _.each( createdModels, function( model ) { |
1288 _.each( created, function( model ) { |
|
1289 model.trigger( 'destroy', model, model.collection, options ); |
1461 model.trigger( 'destroy', model, model.collection, options ); |
1290 options.error && options.error.apply( model, args ); |
|
1291 }); |
1462 }); |
|
1463 |
|
1464 options.error && options.error.apply( models, arguments ); |
1292 }, |
1465 }, |
1293 url: setUrl |
1466 url: setUrl |
1294 }, |
1467 }, |
1295 options |
1468 options |
1296 ); |
1469 ); |
1297 |
1470 |
1298 requests = [ rel.related.fetch( opts ) ]; |
1471 requests = [ coll.fetch( opts ) ]; |
1299 } |
1472 } |
1300 else { |
1473 else { |
|
1474 // Make a request per model to fetch |
|
1475 if ( !models.length ) { |
|
1476 createModels(); |
|
1477 } |
|
1478 |
1301 requests = _.map( models, function( model ) { |
1479 requests = _.map( models, function( model ) { |
1302 var opts = _.defaults( |
1480 var opts = _.defaults( |
1303 { |
1481 { |
1304 error: function() { |
1482 error: function() { |
1305 if ( _.contains( created, model ) ) { |
1483 if ( _.contains( createdModels, model ) ) { |
1306 model.trigger( 'destroy', model, model.collection, options ); |
1484 model.trigger( 'destroy', model, model.collection, options ); |
1307 options.error && options.error.apply( model, arguments ); |
|
1308 } |
1485 } |
|
1486 options.error && options.error.apply( models, arguments ); |
1309 } |
1487 } |
1310 }, |
1488 }, |
1311 options |
1489 options |
1312 ); |
1490 ); |
1313 return model.fetch( opts ); |
1491 return model.fetch( opts ); |
1314 }, this ); |
1492 }, this ); |
1315 } |
1493 } |
1316 } |
1494 } |
1317 |
1495 |
1318 return requests; |
1496 return this.deferArray(requests).then( |
1319 }, |
1497 function() { |
1320 |
1498 return Backbone.Model.prototype.get.call( dit, attr ); |
1321 get: function( attr ) { |
1499 } |
1322 var originalResult = Backbone.Model.prototype.get.call( this, attr ); |
1500 ); |
1323 |
1501 }, |
1324 // Use `originalResult` get if dotNotation not enabled or not required because no dot is in `attr` |
1502 |
1325 if ( !this.dotNotation || attr.indexOf( '.' ) === -1 ) { |
1503 deferArray: function(deferArray) { |
1326 return originalResult; |
1504 return Backbone.$.when.apply(null, deferArray); |
1327 } |
|
1328 |
|
1329 // Go through all splits and return the final result |
|
1330 var splits = attr.split( '.' ); |
|
1331 var result = _.reduce(splits, function( model, split ) { |
|
1332 if ( !( model instanceof Backbone.Model ) ) { |
|
1333 throw new Error( 'Attribute must be an instanceof Backbone.Model. Is: ' + model + ', currentSplit: ' + split ); |
|
1334 } |
|
1335 |
|
1336 return Backbone.Model.prototype.get.call( model, split ); |
|
1337 }, this ); |
|
1338 |
|
1339 if ( originalResult !== undefined && result !== undefined ) { |
|
1340 throw new Error( "Ambiguous result for '" + attr + "'. direct result: " + originalResult + ", dotNotation: " + result ); |
|
1341 } |
|
1342 |
|
1343 return originalResult || result; |
|
1344 }, |
1505 }, |
1345 |
1506 |
1346 set: function( key, value, options ) { |
1507 set: function( key, value, options ) { |
1347 Backbone.Relational.eventQueue.block(); |
1508 Backbone.Relational.eventQueue.block(); |
1348 |
1509 |
1349 // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object |
1510 // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object |
1350 var attributes; |
1511 var attributes, |
|
1512 result; |
|
1513 |
1351 if ( _.isObject( key ) || key == null ) { |
1514 if ( _.isObject( key ) || key == null ) { |
1352 attributes = key; |
1515 attributes = key; |
1353 options = value; |
1516 options = value; |
1354 } |
1517 } |
1355 else { |
1518 else { |
1362 newId = attributes && this.idAttribute in attributes && attributes[ this.idAttribute ]; |
1525 newId = attributes && this.idAttribute in attributes && attributes[ this.idAttribute ]; |
1363 |
1526 |
1364 // Check if we're not setting a duplicate id before actually calling `set`. |
1527 // Check if we're not setting a duplicate id before actually calling `set`. |
1365 Backbone.Relational.store.checkId( this, newId ); |
1528 Backbone.Relational.store.checkId( this, newId ); |
1366 |
1529 |
1367 var result = Backbone.Model.prototype.set.apply( this, arguments ); |
1530 result = Backbone.Model.prototype.set.apply( this, arguments ); |
1368 |
1531 |
1369 // Ideal place to set up relations, if this is the first time we're here for this model |
1532 // Ideal place to set up relations, if this is the first time we're here for this model |
1370 if ( !this._isInitialized && !this.isLocked() ) { |
1533 if ( !this._isInitialized && !this.isLocked() ) { |
1371 this.constructor.initializeModelHierarchy(); |
1534 this.constructor.initializeModelHierarchy(); |
1372 Backbone.Relational.store.register( this ); |
1535 |
|
1536 // Only register models that have an id. A model will be registered when/if it gets an id later on. |
|
1537 if ( newId || newId === 0 ) { |
|
1538 Backbone.Relational.store.register( this ); |
|
1539 } |
|
1540 |
1373 this.initializeRelations( options ); |
1541 this.initializeRelations( options ); |
1374 } |
1542 } |
1375 // The store should know about an `id` update asap |
1543 // The store should know about an `id` update asap |
1376 else if ( newId && newId !== id ) { |
1544 else if ( newId && newId !== id ) { |
1377 Backbone.Relational.store.update( this ); |
1545 Backbone.Relational.store.update( this ); |
1378 } |
1546 } |
1379 |
1547 |
1380 if ( attributes ) { |
1548 if ( attributes ) { |
1381 this.updateRelations( options ); |
1549 this.updateRelations( attributes, options ); |
1382 } |
1550 } |
1383 } |
1551 } |
1384 finally { |
1552 finally { |
1385 // Try to run the global queue holding external events |
1553 // Try to run the global queue holding external events |
1386 Backbone.Relational.eventQueue.unblock(); |
1554 Backbone.Relational.eventQueue.unblock(); |
1387 } |
1555 } |
1388 |
|
1389 return result; |
|
1390 }, |
|
1391 |
|
1392 unset: function( attribute, options ) { |
|
1393 Backbone.Relational.eventQueue.block(); |
|
1394 |
|
1395 var result = Backbone.Model.prototype.unset.apply( this, arguments ); |
|
1396 this.updateRelations( options ); |
|
1397 |
|
1398 // Try to run the global queue holding external events |
|
1399 Backbone.Relational.eventQueue.unblock(); |
|
1400 |
|
1401 return result; |
|
1402 }, |
|
1403 |
|
1404 clear: function( options ) { |
|
1405 Backbone.Relational.eventQueue.block(); |
|
1406 |
|
1407 var result = Backbone.Model.prototype.clear.apply( this, arguments ); |
|
1408 this.updateRelations( options ); |
|
1409 |
|
1410 // Try to run the global queue holding external events |
|
1411 Backbone.Relational.eventQueue.unblock(); |
|
1412 |
1556 |
1413 return result; |
1557 return result; |
1414 }, |
1558 }, |
1415 |
1559 |
1416 clone: function() { |
1560 clone: function() { |
1556 else if ( _.isString( rel.relatedModel ) ) { |
1711 else if ( _.isString( rel.relatedModel ) ) { |
1557 Backbone.Relational.store.addOrphanRelation( rel ); |
1712 Backbone.Relational.store.addOrphanRelation( rel ); |
1558 } |
1713 } |
1559 } |
1714 } |
1560 }, this ); |
1715 }, this ); |
1561 |
1716 |
1562 return this; |
1717 return this; |
1563 }, |
1718 }, |
1564 |
1719 |
1565 /** |
1720 /** |
1566 * Create a 'Backbone.Model' instance based on 'attributes'. |
1721 * Create a 'Backbone.Model' instance based on 'attributes'. |
1567 * @param {Object} attributes |
1722 * @param {Object} attributes |
1568 * @param {Object} [options] |
1723 * @param {Object} [options] |
1569 * @return {Backbone.Model} |
1724 * @return {Backbone.Model} |
1570 */ |
1725 */ |
1571 build: function( attributes, options ) { |
1726 build: function( attributes, options ) { |
1572 var model = this; |
|
1573 |
|
1574 // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet. |
1727 // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet. |
1575 this.initializeModelHierarchy(); |
1728 this.initializeModelHierarchy(); |
1576 |
1729 |
1577 // Determine what type of (sub)model should be built if applicable. |
1730 // Determine what type of (sub)model should be built if applicable. |
1578 // Lookup the proper subModelType in 'this._subModels'. |
1731 var model = this._findSubModelType( this, attributes ) || this; |
1579 if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) { |
1732 |
1580 var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ]; |
1733 return new model( attributes, options ); |
1581 var subModelType = this._subModels[ subModelTypeAttribute ]; |
1734 }, |
|
1735 |
|
1736 /** |
|
1737 * Determines what type of (sub)model should be built if applicable. |
|
1738 * Looks up the proper subModelType in 'this._subModels', recursing into |
|
1739 * types until a match is found. Returns the applicable 'Backbone.Model' |
|
1740 * or null if no match is found. |
|
1741 * @param {Backbone.Model} type |
|
1742 * @param {Object} attributes |
|
1743 * @return {Backbone.Model} |
|
1744 */ |
|
1745 _findSubModelType: function( type, attributes ) { |
|
1746 if ( type._subModels && type.prototype.subModelTypeAttribute in attributes ) { |
|
1747 var subModelTypeAttribute = attributes[ type.prototype.subModelTypeAttribute ]; |
|
1748 var subModelType = type._subModels[ subModelTypeAttribute ]; |
1582 if ( subModelType ) { |
1749 if ( subModelType ) { |
1583 model = subModelType; |
1750 return subModelType; |
1584 } |
1751 } |
1585 } |
1752 else { |
1586 |
1753 // Recurse into subModelTypes to find a match |
1587 return new model( attributes, options ); |
1754 for ( subModelTypeAttribute in type._subModels ) { |
|
1755 subModelType = this._findSubModelType( type._subModels[ subModelTypeAttribute ], attributes ); |
|
1756 if ( subModelType ) { |
|
1757 return subModelType; |
|
1758 } |
|
1759 } |
|
1760 } |
|
1761 } |
|
1762 |
|
1763 return null; |
1588 }, |
1764 }, |
1589 |
1765 |
1590 /** |
1766 /** |
1591 * |
1767 * |
1592 */ |
1768 */ |
1593 initializeModelHierarchy: function() { |
1769 initializeModelHierarchy: function() { |
1594 // If we're here for the first time, try to determine if this modelType has a 'superModel'. |
1770 // Inherit any relations that have been defined in the parent model. |
1595 if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) { |
1771 this.inheritRelations(); |
1596 Backbone.Relational.store.setupSuperModel( this ); |
1772 |
1597 |
1773 // If we came here through 'build' for a model that has 'subModelTypes' then try to initialize the ones that |
1598 // If a superModel has been found, copy relations from the _superModel if they haven't been |
1774 // haven't been resolved yet. |
1599 // inherited automatically (due to a redefinition of 'relations'). |
1775 if ( this.prototype.subModelTypes ) { |
1600 // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail |
1776 var resolvedSubModels = _.keys( this._subModels ); |
1601 // the isUndefined/isNull check next time. |
1777 var unresolvedSubModels = _.omit( this.prototype.subModelTypes, resolvedSubModels ); |
1602 if ( this._superModel && this._superModel.prototype.relations ) { |
1778 _.each( unresolvedSubModels, function( subModelTypeName ) { |
1603 // Find relations that exist on the `_superModel`, but not yet on this model. |
1779 var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName ); |
1604 var inheritedRelations = _.select( this._superModel.prototype.relations || [], function( superRel ) { |
1780 subModelType && subModelType.initializeModelHierarchy(); |
|
1781 }); |
|
1782 } |
|
1783 }, |
|
1784 |
|
1785 inheritRelations: function() { |
|
1786 // Bail out if we've been here before. |
|
1787 if ( !_.isUndefined( this._superModel ) && !_.isNull( this._superModel ) ) { |
|
1788 return; |
|
1789 } |
|
1790 // Try to initialize the _superModel. |
|
1791 Backbone.Relational.store.setupSuperModel( this ); |
|
1792 |
|
1793 // If a superModel has been found, copy relations from the _superModel if they haven't been inherited automatically |
|
1794 // (due to a redefinition of 'relations'). |
|
1795 if ( this._superModel ) { |
|
1796 // The _superModel needs a chance to initialize its own inherited relations before we attempt to inherit relations |
|
1797 // from the _superModel. You don't want to call 'initializeModelHierarchy' because that could cause sub-models of |
|
1798 // this class to inherit their relations before this class has had chance to inherit it's relations. |
|
1799 this._superModel.inheritRelations(); |
|
1800 if ( this._superModel.prototype.relations ) { |
|
1801 // Find relations that exist on the '_superModel', but not yet on this model. |
|
1802 var inheritedRelations = _.filter( this._superModel.prototype.relations || [], function( superRel ) { |
1605 return !_.any( this.prototype.relations || [], function( rel ) { |
1803 return !_.any( this.prototype.relations || [], function( rel ) { |
1606 return superRel.relatedModel === rel.relatedModel && superRel.key === rel.key; |
1804 return superRel.relatedModel === rel.relatedModel && superRel.key === rel.key; |
1607 }, this ); |
1805 }, this ); |
1608 }, this ); |
1806 }, this ); |
1609 |
1807 |
1610 this.prototype.relations = inheritedRelations.concat( this.prototype.relations ); |
1808 this.prototype.relations = inheritedRelations.concat( this.prototype.relations ); |
1611 } |
1809 } |
1612 else { |
1810 } |
1613 this._superModel = false; |
1811 // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail the |
1614 } |
1812 // isUndefined/isNull check next time. |
1615 } |
1813 else { |
1616 |
1814 this._superModel = false; |
1617 // 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. |
|
1618 if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) { |
|
1619 _.each( this.prototype.subModelTypes || [], function( subModelTypeName ) { |
|
1620 var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName ); |
|
1621 subModelType && subModelType.initializeModelHierarchy(); |
|
1622 }); |
|
1623 } |
1815 } |
1624 }, |
1816 }, |
1625 |
1817 |
1626 /** |
1818 /** |
1627 * Find an instance of `this` type in 'Backbone.Relational.store'. |
1819 * Find an instance of `this` type in 'Backbone.Relational.store'. |
1628 * - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found. |
1820 * A new model is created if no matching model is found, `attributes` is an object, and `options.create` is true. |
1629 * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`. |
1821 * - If `attributes` is a string or a number, `findOrCreate` will query the `store` and return a model if found. |
1630 * Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`). |
1822 * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.merge` is `false`. |
1631 * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model. |
1823 * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model. |
1632 * @param {Object} [options] |
1824 * @param {Object} [options] |
1633 * @param {Boolean} [options.create=true] |
1825 * @param {Boolean} [options.create=true] |
1634 * @param {Boolean} [options.merge=true] |
1826 * @param {Boolean} [options.merge=true] |
1635 * @param {Boolean} [options.parse=false] |
1827 * @param {Boolean} [options.parse=false] |
1636 * @return {Backbone.RelationalModel} |
1828 * @return {Backbone.RelationalModel} |
1637 */ |
1829 */ |
1638 findOrCreate: function( attributes, options ) { |
1830 findOrCreate: function( attributes, options ) { |
1639 options || ( options = {} ); |
1831 options || ( options = {} ); |
1640 var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ? |
1832 var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ? |
1641 this.prototype.parse( attributes ) : attributes; |
1833 this.prototype.parse( _.clone( attributes ), options ) : attributes; |
1642 |
1834 |
1643 // Try to find an instance of 'this' model type in the store |
1835 // If specified, use a custom `find` function to match up existing models to the given attributes. |
1644 var model = Backbone.Relational.store.find( this, parsedAttributes ); |
1836 // Otherwise, try to find an instance of 'this' model type in the store |
|
1837 var model = this.findModel( parsedAttributes ); |
1645 |
1838 |
1646 // If we found an instance, update it with the data in 'item' (unless 'options.merge' is false). |
1839 // If we found an instance, update it with the data in 'item' (unless 'options.merge' is false). |
1647 // If not, create an instance (unless 'options.create' is false). |
1840 // If not, create an instance (unless 'options.create' is false). |
1648 if ( _.isObject( attributes ) ) { |
1841 if ( _.isObject( attributes ) ) { |
1649 if ( model && options.merge !== false ) { |
1842 if ( model && options.merge !== false ) { |
1650 // Make sure `options.collection` doesn't cascade to nested models |
1843 // Make sure `options.collection` and `options.url` doesn't cascade to nested models |
1651 delete options.collection; |
1844 delete options.collection; |
|
1845 delete options.url; |
1652 |
1846 |
1653 model.set( parsedAttributes, options ); |
1847 model.set( parsedAttributes, options ); |
1654 } |
1848 } |
1655 else if ( !model && options.create !== false ) { |
1849 else if ( !model && options.create !== false ) { |
1656 model = this.build( attributes, options ); |
1850 model = this.build( parsedAttributes, _.defaults( { parse: false }, options ) ); |
1657 } |
1851 } |
1658 } |
1852 } |
1659 |
1853 |
1660 return model; |
1854 return model; |
|
1855 }, |
|
1856 |
|
1857 /** |
|
1858 * Find an instance of `this` type in 'Backbone.Relational.store'. |
|
1859 * - If `attributes` is a string or a number, `find` will query the `store` and return a model if found. |
|
1860 * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.merge` is `false`. |
|
1861 * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model. |
|
1862 * @param {Object} [options] |
|
1863 * @param {Boolean} [options.merge=true] |
|
1864 * @param {Boolean} [options.parse=false] |
|
1865 * @return {Backbone.RelationalModel} |
|
1866 */ |
|
1867 find: function( attributes, options ) { |
|
1868 options || ( options = {} ); |
|
1869 options.create = false; |
|
1870 return this.findOrCreate( attributes, options ); |
|
1871 }, |
|
1872 |
|
1873 /** |
|
1874 * A hook to override the matching when updating (or creating) a model. |
|
1875 * The default implementation is to look up the model by id in the store. |
|
1876 * @param {Object} attributes |
|
1877 * @returns {Backbone.RelationalModel} |
|
1878 */ |
|
1879 findModel: function( attributes ) { |
|
1880 return Backbone.Relational.store.find( this, attributes ); |
1661 } |
1881 } |
1662 }); |
1882 }); |
1663 _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore ); |
1883 _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore ); |
1664 |
1884 |
1665 /** |
1885 /** |
1705 */ |
1925 */ |
1706 var set = Backbone.Collection.prototype.__set = Backbone.Collection.prototype.set; |
1926 var set = Backbone.Collection.prototype.__set = Backbone.Collection.prototype.set; |
1707 Backbone.Collection.prototype.set = function( models, options ) { |
1927 Backbone.Collection.prototype.set = function( models, options ) { |
1708 // Short-circuit if this Collection doesn't hold RelationalModels |
1928 // Short-circuit if this Collection doesn't hold RelationalModels |
1709 if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) { |
1929 if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) { |
1710 return set.apply( this, arguments ); |
1930 return set.call( this, models, options ); |
1711 } |
1931 } |
1712 |
1932 |
1713 if ( options && options.parse ) { |
1933 if ( options && options.parse ) { |
1714 models = this.parse( models, options ); |
1934 models = this.parse( models, options ); |
1715 } |
1935 } |
1716 |
1936 |
1717 if ( !_.isArray( models ) ) { |
1937 var singular = !_.isArray( models ), |
1718 models = models ? [ models ] : []; |
1938 newModels = [], |
1719 } |
1939 toAdd = [], |
1720 |
1940 model = null; |
1721 var newModels = [], |
1941 |
1722 toAdd = []; |
1942 models = singular ? ( models ? [ models ] : [] ) : _.clone( models ); |
1723 |
1943 |
1724 //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options ); |
1944 //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options ); |
1725 _.each( models, function( model ) { |
1945 for ( var i = 0; i < models.length; i++ ) { |
|
1946 model = models[i]; |
1726 if ( !( model instanceof Backbone.Model ) ) { |
1947 if ( !( model instanceof Backbone.Model ) ) { |
1727 model = Backbone.Collection.prototype._prepareModel.call( this, model, options ); |
1948 model = Backbone.Collection.prototype._prepareModel.call( this, model, options ); |
1728 } |
1949 } |
1729 |
|
1730 if ( model ) { |
1950 if ( model ) { |
1731 toAdd.push( model ); |
1951 toAdd.push( model ); |
1732 |
|
1733 if ( !( this.get( model ) || this.get( model.cid ) ) ) { |
1952 if ( !( this.get( model ) || this.get( model.cid ) ) ) { |
1734 newModels.push( model ); |
1953 newModels.push( model ); |
1735 } |
1954 } |
1736 // If we arrive in `add` while performing a `set` (after a create, so the model gains an `id`), |
1955 // If we arrive in `add` while performing a `set` (after a create, so the model gains an `id`), |
1737 // we may get here before `_onModelEvent` has had the chance to update `_byId`. |
1956 // we may get here before `_onModelEvent` has had the chance to update `_byId`. |
1738 else if ( model.id != null ) { |
1957 else if ( model.id !== null && model.id !== undefined ) { |
1739 this._byId[ model.id ] = model; |
1958 this._byId[ model.id ] = model; |
1740 } |
1959 } |
1741 } |
1960 } |
1742 }, this ); |
1961 } |
1743 |
1962 |
1744 // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc). |
1963 // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc). |
1745 // If `parse` was specified, the collection and contained models have been parsed now. |
1964 // If `parse` was specified, the collection and contained models have been parsed now. |
1746 set.call( this, toAdd, _.defaults( { parse: false }, options ) ); |
1965 toAdd = singular ? ( toAdd.length ? toAdd[ 0 ] : null ) : toAdd; |
1747 |
1966 var result = set.call( this, toAdd, _.defaults( { merge: false, parse: false }, options ) ); |
1748 _.each( newModels, function( model ) { |
1967 |
|
1968 for ( i = 0; i < newModels.length; i++ ) { |
|
1969 model = newModels[i]; |
1749 // Fire a `relational:add` event for any model in `newModels` that has actually been added to the collection. |
1970 // Fire a `relational:add` event for any model in `newModels` that has actually been added to the collection. |
1750 if ( this.get( model ) || this.get( model.cid ) ) { |
1971 if ( this.get( model ) || this.get( model.cid ) ) { |
1751 this.trigger( 'relational:add', model, this, options ); |
1972 this.trigger( 'relational:add', model, this, options ); |
1752 } |
1973 } |
1753 }, this ); |
1974 } |
1754 |
1975 |
1755 return this; |
1976 return result; |
1756 }; |
1977 }; |
1757 |
1978 |
1758 /** |
1979 /** |
1759 * Override 'Backbone.Collection.remove' to trigger 'relational:remove'. |
1980 * Override 'Backbone.Collection._removeModels' to trigger 'relational:remove'. |
1760 */ |
1981 */ |
1761 var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove; |
1982 var _removeModels = Backbone.Collection.prototype.___removeModels = Backbone.Collection.prototype._removeModels; |
1762 Backbone.Collection.prototype.remove = function( models, options ) { |
1983 Backbone.Collection.prototype._removeModels = function( models, options ) { |
1763 // Short-circuit if this Collection doesn't hold RelationalModels |
1984 // Short-circuit if this Collection doesn't hold RelationalModels |
1764 if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) { |
1985 if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) { |
1765 return remove.apply( this, arguments ); |
1986 return _removeModels.call( this, models, options ); |
1766 } |
1987 } |
1767 |
|
1768 models = _.isArray( models ) ? models.slice() : [ models ]; |
|
1769 options || ( options = {} ); |
|
1770 |
1988 |
1771 var toRemove = []; |
1989 var toRemove = []; |
1772 |
1990 |
1773 //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options ); |
1991 //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options ); |
1774 _.each( models, function( model ) { |
1992 _.each( models, function( model ) { |
1775 model = this.get( model ) || this.get( model.cid ); |
1993 model = this.get( model ) || ( model && this.get( model.cid ) ); |
1776 model && toRemove.push( model ); |
1994 model && toRemove.push( model ); |
1777 }, this ); |
1995 }, this ); |
1778 |
1996 |
1779 if ( toRemove.length ) { |
1997 var result = _removeModels.call( this, toRemove, options ); |
1780 remove.call( this, toRemove, options ); |
1998 |
1781 |
1999 _.each( toRemove, function( model ) { |
1782 _.each( toRemove, function( model ) { |
2000 this.trigger( 'relational:remove', model, this, options ); |
1783 this.trigger('relational:remove', model, this, options); |
2001 }, this ); |
1784 }, this ); |
2002 |
1785 } |
2003 return result; |
1786 |
|
1787 return this; |
|
1788 }; |
2004 }; |
1789 |
2005 |
1790 /** |
2006 /** |
1791 * Override 'Backbone.Collection.reset' to trigger 'relational:reset'. |
2007 * Override 'Backbone.Collection.reset' to trigger 'relational:reset'. |
1792 */ |
2008 */ |
1793 var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset; |
2009 var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset; |
1794 Backbone.Collection.prototype.reset = function( models, options ) { |
2010 Backbone.Collection.prototype.reset = function( models, options ) { |
1795 options = _.extend( { merge: true }, options ); |
2011 options = _.extend( { merge: true }, options ); |
1796 reset.call( this, models, options ); |
2012 var result = reset.call( this, models, options ); |
1797 |
2013 |
1798 if ( this.model.prototype instanceof Backbone.RelationalModel ) { |
2014 if ( this.model.prototype instanceof Backbone.RelationalModel ) { |
1799 this.trigger( 'relational:reset', this, options ); |
2015 this.trigger( 'relational:reset', this, options ); |
1800 } |
2016 } |
1801 |
2017 |
1802 return this; |
2018 return result; |
1803 }; |
2019 }; |
1804 |
2020 |
1805 /** |
2021 /** |
1806 * Override 'Backbone.Collection.sort' to trigger 'relational:reset'. |
2022 * Override 'Backbone.Collection.sort' to trigger 'relational:reset'. |
1807 */ |
2023 */ |
1808 var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort; |
2024 var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort; |
1809 Backbone.Collection.prototype.sort = function( options ) { |
2025 Backbone.Collection.prototype.sort = function( options ) { |
1810 sort.call( this, options ); |
2026 var result = sort.call( this, options ); |
1811 |
2027 |
1812 if ( this.model.prototype instanceof Backbone.RelationalModel ) { |
2028 if ( this.model.prototype instanceof Backbone.RelationalModel ) { |
1813 this.trigger( 'relational:reset', this, options ); |
2029 this.trigger( 'relational:reset', this, options ); |
1814 } |
2030 } |
1815 |
2031 |
1816 return this; |
2032 return result; |
1817 }; |
2033 }; |
1818 |
2034 |
1819 /** |
2035 /** |
1820 * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations |
2036 * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations |
1821 * are ready. |
2037 * are ready. |