backbone-relational
Advanced tools
Comparing version 0.7.0 to 0.8.0
@@ -1,5 +0,5 @@ | ||
/* vim: set tabstop=4:softtabstop=4:shiftwidth=4:noexpandtab */ | ||
/* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */ | ||
/** | ||
* Backbone-relational.js 0.7.0 | ||
* (c) 2011-2013 Paul Uithol | ||
* Backbone-relational.js 0.8.0 | ||
* (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors) | ||
* | ||
@@ -12,3 +12,3 @@ * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt. | ||
"use strict"; | ||
/** | ||
@@ -39,3 +39,3 @@ * CommonJS shim | ||
_permitsUsed: 0, | ||
acquire: function() { | ||
@@ -49,3 +49,3 @@ if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) { | ||
}, | ||
release: function() { | ||
@@ -59,7 +59,7 @@ if ( this._permitsUsed === 0 ) { | ||
}, | ||
isLocked: function() { | ||
return this._permitsUsed > 0; | ||
}, | ||
setAvailablePermits: function( amount ) { | ||
@@ -72,3 +72,3 @@ if ( this._permitsUsed > amount ) { | ||
}; | ||
/** | ||
@@ -84,3 +84,3 @@ * A BlockingQueue that accumulates items while blocked (via 'block'), | ||
_queue: null, | ||
add: function( func ) { | ||
@@ -94,3 +94,3 @@ if ( this.isBlocked() ) { | ||
}, | ||
process: function() { | ||
@@ -101,7 +101,7 @@ while ( this._queue && this._queue.length ) { | ||
}, | ||
block: function() { | ||
this.acquire(); | ||
}, | ||
unblock: function() { | ||
@@ -113,3 +113,3 @@ this.release(); | ||
}, | ||
isBlocked: function() { | ||
@@ -120,7 +120,7 @@ return this.isLocked(); | ||
/** | ||
* Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'update:<key>') | ||
* Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'change:<key>') | ||
* until the top-level object is fully initialized (see 'Backbone.RelationalModel'). | ||
*/ | ||
Backbone.Relational.eventQueue = new Backbone.BlockingQueue(); | ||
/** | ||
@@ -133,2 +133,3 @@ * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel. | ||
this._reverseRelations = []; | ||
this._orphanRelations = []; | ||
this._subModels = []; | ||
@@ -138,2 +139,22 @@ this._modelScopes = [ exports ]; | ||
_.extend( Backbone.Store.prototype, Backbone.Events, { | ||
/** | ||
* Create a new `Relation`. | ||
* @param {Backbone.RelationalModel} [model] | ||
* @param {Object} relation | ||
* @param {Object} [options] | ||
*/ | ||
initializeRelation: function( model, relation, options ) { | ||
var type = !_.isString( relation.type ) ? relation.type : Backbone[ relation.type ] || this.getObjectByName( relation.type ); | ||
if ( type && type.prototype instanceof Backbone.Relation ) { | ||
new type( model, relation, options ); // Also pushes the new Relation into `model._relations` | ||
} | ||
else { | ||
Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid relation type!', relation ); | ||
} | ||
}, | ||
/** | ||
* Add a scope for `getObjectByName` to look for model types by name. | ||
* @param {Object} scope | ||
*/ | ||
addModelScope: function( scope ) { | ||
@@ -164,3 +185,3 @@ this._modelScopes.push( scope ); | ||
setupSuperModel: function( modelType ) { | ||
_.find( this._subModels || [], function( subModelDef ) { | ||
_.find( this._subModels, function( subModelDef ) { | ||
return _.find( subModelDef.subModels || [], function( subModelTypeName, typeValue ) { | ||
@@ -182,3 +203,3 @@ var subModelType = this.getObjectByName( subModelTypeName ); | ||
}, | ||
/** | ||
@@ -194,29 +215,64 @@ * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to | ||
addReverseRelation: function( relation ) { | ||
var exists = _.any( this._reverseRelations || [], function( rel ) { | ||
return _.all( relation || [], function( val, key ) { | ||
return val === rel[ key ]; | ||
}); | ||
var exists = _.any( this._reverseRelations, function( rel ) { | ||
return _.all( relation || [], function( val, key ) { | ||
return val === rel[ key ]; | ||
}); | ||
}); | ||
if ( !exists && relation.model && relation.type ) { | ||
this._reverseRelations.push( relation ); | ||
var addRelation = function( model, relation ) { | ||
if ( !model.prototype.relations ) { | ||
model.prototype.relations = []; | ||
} | ||
model.prototype.relations.push( relation ); | ||
_.each( model._subModels || [], function( subModel ) { | ||
addRelation( subModel, relation ); | ||
}, this ); | ||
}; | ||
addRelation( relation.model, relation ); | ||
this._addRelation( relation.model, relation ); | ||
this.retroFitRelation( relation ); | ||
} | ||
}, | ||
/** | ||
* Deposit a `relation` for which the `relatedModel` can't be resolved at the moment. | ||
* | ||
* @param {Object} relation | ||
*/ | ||
addOrphanRelation: function( relation ) { | ||
var exists = _.any( this._orphanRelations, function( rel ) { | ||
return _.all( relation || [], function( val, key ) { | ||
return val === rel[ key ]; | ||
}); | ||
}); | ||
if ( !exists && relation.model && relation.type ) { | ||
this._orphanRelations.push( relation ); | ||
} | ||
}, | ||
/** | ||
* Try to initialize any `_orphanRelation`s | ||
*/ | ||
processOrphanRelations: function() { | ||
// Make sure to operate on a copy since we're removing while iterating | ||
_.each( this._orphanRelations.slice( 0 ), function( rel ) { | ||
var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel ); | ||
if ( relatedModel ) { | ||
this.initializeRelation( null, rel ); | ||
this._orphanRelations = _.without( this._orphanRelations, rel ); | ||
} | ||
}, this ); | ||
}, | ||
/** | ||
* | ||
* @param {Backbone.RelationalModel.constructor} type | ||
* @param {Object} relation | ||
* @private | ||
*/ | ||
_addRelation: function( type, relation ) { | ||
if ( !type.prototype.relations ) { | ||
type.prototype.relations = []; | ||
} | ||
type.prototype.relations.push( relation ); | ||
_.each( type._subModels || [], function( subModel ) { | ||
this._addRelation( subModel, relation ); | ||
}, this ); | ||
}, | ||
/** | ||
* Add a 'relation' to all existing instances of 'relation.model' in the store | ||
@@ -226,4 +282,4 @@ * @param {Object} relation | ||
retroFitRelation: function( relation ) { | ||
var coll = this.getCollection( relation.model ); | ||
coll.each( function( model ) { | ||
var coll = this.getCollection( relation.model, false ); | ||
coll && coll.each( function( model ) { | ||
if ( !( model instanceof relation.model ) ) { | ||
@@ -234,16 +290,17 @@ return; | ||
new relation.type( model, relation ); | ||
}, this); | ||
}, this ); | ||
}, | ||
/** | ||
* Find the Store's collection for a certain type of model. | ||
* @param {Backbone.RelationalModel} model | ||
* @param {Backbone.RelationalModel} type | ||
* @param {Boolean} [create=true] Should a collection be created if none is found? | ||
* @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null | ||
*/ | ||
getCollection: function( model ) { | ||
if ( model instanceof Backbone.RelationalModel ) { | ||
model = model.constructor; | ||
getCollection: function( type, create ) { | ||
if ( type instanceof Backbone.RelationalModel ) { | ||
type = type.constructor; | ||
} | ||
var rootModel = model; | ||
var rootModel = type; | ||
while ( rootModel._superModel ) { | ||
@@ -253,7 +310,5 @@ rootModel = rootModel._superModel; | ||
var coll = _.detect( this._collections, function( c ) { | ||
return c.model === rootModel; | ||
}); | ||
var coll = _.findWhere( this._collections, { model: rootModel } ); | ||
if ( !coll ) { | ||
if ( !coll && create !== false ) { | ||
coll = this._createCollection( rootModel ); | ||
@@ -264,5 +319,5 @@ } | ||
}, | ||
/** | ||
* Find a type on the global object by name. Splits name on dots. | ||
* Find a model type on one of the modelScopes by name. Names are split on dots. | ||
* @param {String} name | ||
@@ -275,3 +330,3 @@ * @return {Object} | ||
_.find( this._modelScopes || [], function( scope ) { | ||
_.find( this._modelScopes, function( scope ) { | ||
type = _.reduce( parts || [], function( memo, val ) { | ||
@@ -288,3 +343,3 @@ return memo ? memo[ val ] : undefined; | ||
}, | ||
_createCollection: function( type ) { | ||
@@ -356,5 +411,5 @@ var coll; | ||
}, | ||
/** | ||
* Add a 'model' to it's appropriate collection. Retain the original contents of 'model.collection'. | ||
* Add a 'model' to its appropriate collection. Retain the original contents of 'model.collection'. | ||
* @param {Backbone.RelationalModel} model | ||
@@ -367,2 +422,5 @@ */ | ||
if ( coll.get( model ) ) { | ||
if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) { | ||
console.warn( 'Duplicate id! Old RelationalModel=%o, new RelationalModel=%o', coll.get( model ), model ); | ||
} | ||
throw new Error( "Cannot instantiate more than one Backbone.RelationalModel with the same id per type!" ); | ||
@@ -373,11 +431,11 @@ } | ||
coll.add( model ); | ||
model.bind( 'destroy', this.unregister, this ); | ||
this.listenTo( model, 'destroy', this.unregister, this ); | ||
model.collection = modelColl; | ||
} | ||
}, | ||
/** | ||
* Explicitly update a model's id in it's store collection | ||
* Explicitly update a model's id in its store collection | ||
* @param {Backbone.RelationalModel} model | ||
*/ | ||
*/ | ||
update: function( model ) { | ||
@@ -387,3 +445,3 @@ var coll = this.getCollection( model ); | ||
}, | ||
/** | ||
@@ -394,9 +452,20 @@ * Remove a 'model' from the store. | ||
unregister: function( model ) { | ||
model.unbind( 'destroy', this.unregister ); | ||
this.stopListening( model, 'destroy', this.unregister ); | ||
var coll = this.getCollection( model ); | ||
coll && coll.remove( model ); | ||
}, | ||
/** | ||
* Reset the `store` to it's original state. The `reverseRelations` are kept though, since attempting to | ||
* re-initialize these on models would lead to a large amount of warnings. | ||
*/ | ||
reset: function() { | ||
this.stopListening(); | ||
this._collections = []; | ||
this._subModels = []; | ||
this._modelScopes = [ exports ]; | ||
} | ||
}); | ||
Backbone.Relational.store = new Backbone.Store(); | ||
/** | ||
@@ -406,3 +475,4 @@ * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:<key>' events | ||
* | ||
* @param {Backbone.RelationalModel} instance | ||
* @param {Backbone.RelationalModel} [instance] Model that this relation is created for. If no model is supplied, | ||
* Relation just tries to instantiate it's `reverseRelation` if specified, and bails out after that. | ||
* @param {Object} options | ||
@@ -416,4 +486,5 @@ * @param {string} options.key | ||
* {Backbone.Relation|String} type ('HasOne' or 'HasMany'). | ||
* @param {Object} opts | ||
*/ | ||
Backbone.Relation = function( instance, options ) { | ||
Backbone.Relation = function( instance, options, opts ) { | ||
this.instance = instance; | ||
@@ -423,7 +494,7 @@ // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype | ||
this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation ); | ||
this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options ); | ||
this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type : | ||
Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type ); | ||
this.model = options.model || this.instance.constructor; | ||
this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options ); | ||
this.key = this.options.key; | ||
@@ -433,3 +504,3 @@ this.keySource = this.options.keySource || this.key; | ||
// 'exports' should be the global object where 'relatedModel' can be found on if given as a string. | ||
this.model = this.options.model || this.instance.constructor; | ||
this.relatedModel = this.options.relatedModel; | ||
@@ -444,2 +515,14 @@ if ( _.isString( this.relatedModel ) ) { | ||
// Add the reverse relation on 'relatedModel' to the store's reverseRelations | ||
if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) { | ||
Backbone.Relational.store.addReverseRelation( _.defaults( { | ||
isAutoRelation: true, | ||
model: this.relatedModel, | ||
relatedModel: this.model, | ||
reverseRelation: this.options // current relation is the 'reverseRelation' for its own reverseRelation | ||
}, | ||
this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.) | ||
) ); | ||
} | ||
if ( instance ) { | ||
@@ -451,3 +534,4 @@ var contentKey = this.keySource; | ||
this.keyContents = this.instance.get( contentKey ); | ||
this.setKeyContents( this.instance.get( contentKey ) ); | ||
this.relatedCollection = Backbone.Relational.store.getCollection( this.relatedModel ); | ||
@@ -460,34 +544,14 @@ // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'. | ||
// Add this Relation to instance._relations | ||
this.instance._relations.push( this ); | ||
} | ||
this.instance._relations[ this.key ] = this; | ||
// Add the reverse relation on 'relatedModel' to the store's reverseRelations | ||
if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) { | ||
Backbone.Relational.store.addReverseRelation( _.defaults( { | ||
isAutoRelation: true, | ||
model: this.relatedModel, | ||
relatedModel: this.model, | ||
reverseRelation: this.options // current relation is the 'reverseRelation' for it's own reverseRelation | ||
}, | ||
this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.) | ||
) ); | ||
} | ||
this.initialize( opts ); | ||
_.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' ); | ||
if ( instance ) { | ||
this.initialize(); | ||
if ( options.autoFetch ) { | ||
this.instance.fetchRelated( options.key, _.isObject( options.autoFetch ) ? options.autoFetch : {} ); | ||
if ( this.options.autoFetch ) { | ||
this.instance.fetchRelated( this.key, _.isObject( this.options.autoFetch ) ? this.options.autoFetch : {} ); | ||
} | ||
// When a model in the store is destroyed, check if it is 'this.instance'. | ||
Backbone.Relational.store.getCollection( this.instance ) | ||
.bind( 'relational:remove', this._modelRemovedFromCollection ); | ||
// When 'relatedModel' are created or destroyed, check if it affects this relation. | ||
Backbone.Relational.store.getCollection( this.relatedModel ) | ||
.bind( 'relational:add', this._relatedModelAdded ) | ||
.bind( 'relational:remove', this._relatedModelRemoved ); | ||
this.listenTo( this.instance, 'destroy', this.destroy ) | ||
.listenTo( this.relatedCollection, 'relational:add', this.tryAddRelated ) | ||
.listenTo( this.relatedCollection, 'relational:remove', this.removeRelated ) | ||
} | ||
@@ -503,5 +567,6 @@ }; | ||
isAutoRelation: false, | ||
autoFetch: false | ||
autoFetch: false, | ||
parse: false | ||
}, | ||
instance: null, | ||
@@ -511,24 +576,6 @@ key: null, | ||
relatedModel: null, | ||
relatedCollection: null, | ||
reverseRelation: null, | ||
related: null, | ||
_relatedModelAdded: function( model, coll, options ) { | ||
// Allow 'model' to set up it's relations, before calling 'tryAddRelated' | ||
// (which can result in a call to 'addRelated' on a relation of 'model') | ||
var dit = this; | ||
model.queue( function() { | ||
dit.tryAddRelated( model, options ); | ||
}); | ||
}, | ||
_relatedModelRemoved: function( model, coll, options ) { | ||
this.removeRelated( model, options ); | ||
}, | ||
_modelRemovedFromCollection: function( model ) { | ||
if ( model === this.instance ) { | ||
this.destroy(); | ||
} | ||
}, | ||
/** | ||
@@ -546,3 +593,3 @@ * Check several pre-conditions. | ||
if ( !m || !k || !rm ) { | ||
warn && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, m, k, rm ); | ||
warn && console.warn( 'Relation=%o: missing model, key or relatedModel (%o, %o, %o).', this, m, k, rm ); | ||
return false; | ||
@@ -552,3 +599,3 @@ } | ||
if ( !( m.prototype instanceof Backbone.RelationalModel ) ) { | ||
warn && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i ); | ||
warn && console.warn( 'Relation=%o: model does not inherit from Backbone.RelationalModel (%o).', this, i ); | ||
return false; | ||
@@ -558,3 +605,3 @@ } | ||
if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) { | ||
warn && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm ); | ||
warn && console.warn( 'Relation=%o: relatedModel does not inherit from Backbone.RelationalModel (%o).', this, rm ); | ||
return false; | ||
@@ -564,17 +611,14 @@ } | ||
if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) { | ||
warn && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this ); | ||
warn && console.warn( 'Relation=%o: relation is a HasMany, and the reverseRelation is HasMany as well.', this ); | ||
return false; | ||
} | ||
// Check if we're not attempting to create a relationship on a `key` that's already used. | ||
if ( i && _.keys( i._relations ).length ) { | ||
var existing = _.find( i._relations, function( rel ) { | ||
return rel.key === k; | ||
}, this ); | ||
// Check if we're not attempting to create a duplicate relationship | ||
if ( i && i._relations.length ) { | ||
var exists = _.any( i._relations || [], function( rel ) { | ||
var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key; | ||
return rel.relatedModel === rm && rel.key === k && | ||
( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key ); | ||
}, this ); | ||
if ( exists ) { | ||
warn && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists', | ||
this, i, k, rm, this.reverseRelation.key ); | ||
if ( existing ) { | ||
warn && console.warn( 'Cannot create relation=%o on %o for model=%o: already taken by relation=%o.', | ||
this, k, i, existing ); | ||
return false; | ||
@@ -590,12 +634,11 @@ } | ||
* @param {Backbone.Model|Backbone.Collection} related | ||
* @param {Object} [options] | ||
*/ | ||
setRelated: function( related, options ) { | ||
setRelated: function( related ) { | ||
this.related = related; | ||
this.instance.acquire(); | ||
this.instance.set( this.key, related, _.defaults( options || {}, { silent: true } ) ); | ||
this.instance.attributes[ this.key ] = related; | ||
this.instance.release(); | ||
}, | ||
/** | ||
@@ -608,9 +651,6 @@ * Determine if a relation (on a different RelationalModel) is the reverse | ||
_isReverseRelation: function( relation ) { | ||
if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key && | ||
this.key === relation.reverseRelation.key ) { | ||
return true; | ||
} | ||
return false; | ||
return relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key && | ||
this.key === relation.reverseRelation.key; | ||
}, | ||
/** | ||
@@ -627,57 +667,32 @@ * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s). | ||
_.each( models || [], function( related ) { | ||
_.each( related.getRelations() || [], function( relation ) { | ||
if ( this._isReverseRelation( relation ) ) { | ||
reverseRelations.push( relation ); | ||
} | ||
}, this ); | ||
}, this ); | ||
_.each( related.getRelations() || [], function( relation ) { | ||
if ( this._isReverseRelation( relation ) ) { | ||
reverseRelations.push( relation ); | ||
} | ||
}, this ); | ||
}, this ); | ||
return reverseRelations; | ||
}, | ||
/** | ||
* Rename options.silent to options.silentChange, so events propagate properly. | ||
* (for example in HasMany, from 'addRelated'->'handleAddition') | ||
* @param {Object} [options] | ||
* @return {Object} | ||
* When `this.instance` is destroyed, cleanup our relations. | ||
* Get reverse relation, call removeRelated on each. | ||
*/ | ||
sanitizeOptions: function( options ) { | ||
options = options ? _.clone( options ) : {}; | ||
if ( options.silent ) { | ||
options.silentChange = true; | ||
delete options.silent; | ||
} | ||
return options; | ||
}, | ||
destroy: function() { | ||
this.stopListening(); | ||
/** | ||
* Rename options.silentChange to options.silent, so events are silenced as intended in Backbone's | ||
* original functions. | ||
* @param {Object} [options] | ||
* @return {Object} | ||
*/ | ||
unsanitizeOptions: function( options ) { | ||
options = options ? _.clone( options ) : {}; | ||
if ( options.silentChange ) { | ||
options.silent = true; | ||
delete options.silentChange; | ||
if ( this instanceof Backbone.HasOne ) { | ||
this.setRelated( null ); | ||
} | ||
return options; | ||
}, | ||
// Cleanup. Get reverse relation, call removeRelated on each. | ||
destroy: function() { | ||
Backbone.Relational.store.getCollection( this.instance ) | ||
.unbind( 'relational:remove', this._modelRemovedFromCollection ); | ||
else if ( this instanceof Backbone.HasMany ) { | ||
this.setRelated( this._prepareCollection() ); | ||
} | ||
Backbone.Relational.store.getCollection( this.relatedModel ) | ||
.unbind( 'relational:add', this._relatedModelAdded ) | ||
.unbind( 'relational:remove', this._relatedModelRemoved ); | ||
_.each( this.getReverseRelations() || [], function( relation ) { | ||
relation.removeRelated( this.instance ); | ||
}, this ); | ||
_.each( this.getReverseRelations(), function( relation ) { | ||
relation.removeRelated( this.instance ); | ||
}, this ); | ||
} | ||
}); | ||
Backbone.HasOne = Backbone.Relation.extend({ | ||
@@ -687,34 +702,49 @@ options: { | ||
}, | ||
initialize: function() { | ||
_.bindAll( this, 'onChange' ); | ||
this.instance.bind( 'relational:change:' + this.key, this.onChange ); | ||
initialize: function( opts ) { | ||
this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange ); | ||
var model = this.findRelated( { silent: true } ); | ||
this.setRelated( model ); | ||
var related = this.findRelated( opts ); | ||
this.setRelated( related ); | ||
// Notify new 'related' object of the new relation. | ||
_.each( this.getReverseRelations() || [], function( relation ) { | ||
relation.addRelated( this.instance ); | ||
}, this ); | ||
_.each( this.getReverseRelations(), function( relation ) { | ||
relation.addRelated( this.instance, opts ); | ||
}, this ); | ||
}, | ||
/** | ||
* Find related Models. | ||
* @param {Object} [options] | ||
* @return {Backbone.Model} | ||
*/ | ||
findRelated: function( options ) { | ||
var item = this.keyContents; | ||
var model = null; | ||
if ( item instanceof this.relatedModel ) { | ||
model = item; | ||
var related = null; | ||
options = _.defaults( { parse: this.options.parse }, options ); | ||
if ( this.keyContents instanceof this.relatedModel ) { | ||
related = this.keyContents; | ||
} | ||
else if ( item || item === 0 ) { // since 0 can be a valid `id` as well | ||
model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } ); | ||
else if ( this.keyContents || this.keyContents === 0 ) { // since 0 can be a valid `id` as well | ||
var opts = _.defaults( { create: this.options.createModels }, options ); | ||
related = this.relatedModel.findOrCreate( this.keyContents, opts ); | ||
} | ||
return model; | ||
return related; | ||
}, | ||
/** | ||
* If the key is changed, notify old & new reverse relations and initialize the new relation | ||
* Normalize and reduce `keyContents` to an `id`, for easier comparison | ||
* @param {String|Number|Backbone.Model} keyContents | ||
*/ | ||
setKeyContents: function( keyContents ) { | ||
this.keyContents = keyContents; | ||
this.keyId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, this.keyContents ); | ||
}, | ||
/** | ||
* Event handler for `change:<key>`. | ||
* If the key is changed, notify old & new reverse relations and initialize the new relation. | ||
*/ | ||
onChange: function( model, attr, options ) { | ||
@@ -726,24 +756,14 @@ // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange) | ||
this.acquire(); | ||
options = this.sanitizeOptions( options ); | ||
options = options ? _.clone( options ) : {}; | ||
// 'options._related' is set by 'addRelated'/'removeRelated'. If it is set, the change | ||
// 'options.__related' is set by 'addRelated'/'removeRelated'. If it is set, the change | ||
// is the result of a call from a relation. If it's not, the change is the result of | ||
// a 'set' call on this.instance. | ||
var changed = _.isUndefined( options._related ); | ||
var oldRelated = changed ? this.related : options._related; | ||
var changed = _.isUndefined( options.__related ), | ||
oldRelated = changed ? this.related : options.__related; | ||
if ( changed ) { | ||
this.keyContents = attr; | ||
// Set new 'related' | ||
if ( attr instanceof this.relatedModel ) { | ||
this.related = attr; | ||
} | ||
else if ( attr ) { | ||
var related = this.findRelated( options ); | ||
this.setRelated( related ); | ||
} | ||
else { | ||
this.setRelated( null ); | ||
} | ||
if ( changed ) { | ||
this.setKeyContents( attr ); | ||
var related = this.findRelated( options ); | ||
this.setRelated( related ); | ||
} | ||
@@ -753,19 +773,21 @@ | ||
if ( oldRelated && this.related !== oldRelated ) { | ||
_.each( this.getReverseRelations( oldRelated ) || [], function( relation ) { | ||
relation.removeRelated( this.instance, options ); | ||
}, this ); | ||
_.each( this.getReverseRelations( oldRelated ), function( relation ) { | ||
relation.removeRelated( this.instance, null, options ); | ||
}, this ); | ||
} | ||
// Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated; | ||
// that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'. | ||
// In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet. | ||
_.each( this.getReverseRelations() || [], function( relation ) { | ||
relation.addRelated( this.instance, options ); | ||
}, this); | ||
_.each( this.getReverseRelations(), function( relation ) { | ||
relation.addRelated( this.instance, options ); | ||
}, this ); | ||
// Fire the 'update:<key>' event if 'related' was updated | ||
if ( !options.silentChange && this.related !== oldRelated ) { | ||
// Fire the 'change:<key>' event if 'related' was updated | ||
if ( !options.silent && this.related !== oldRelated ) { | ||
var dit = this; | ||
this.changed = true; | ||
Backbone.Relational.eventQueue.add( function() { | ||
dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options ); | ||
dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true ); | ||
dit.changed = false; | ||
}); | ||
@@ -775,30 +797,27 @@ } | ||
}, | ||
/** | ||
* If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents' | ||
*/ | ||
tryAddRelated: function( model, options ) { | ||
if ( this.related ) { | ||
return; | ||
tryAddRelated: function( model, coll, options ) { | ||
if ( ( this.keyId || this.keyId === 0 ) && model.id === this.keyId ) { // since 0 can be a valid `id` as well | ||
this.addRelated( model, options ); | ||
this.keyId = null; | ||
} | ||
options = this.sanitizeOptions( options ); | ||
var item = this.keyContents; | ||
if ( item || item === 0 ) { // since 0 can be a valid `id` as well | ||
var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item ); | ||
if ( !_.isNull( id ) && model.id === id ) { | ||
this.addRelated( model, options ); | ||
} | ||
} | ||
}, | ||
addRelated: function( model, options ) { | ||
if ( model !== this.related ) { | ||
var oldRelated = this.related || null; | ||
this.setRelated( model ); | ||
this.onChange( this.instance, model, { _related: oldRelated } ); | ||
} | ||
// Allow 'model' to set up its relations before proceeding. | ||
// (which can result in a call to 'addRelated' from a relation of 'model') | ||
var dit = this; | ||
model.queue( function() { | ||
if ( model !== dit.related ) { | ||
var oldRelated = dit.related || null; | ||
dit.setRelated( model ); | ||
dit.onChange( dit.instance, model, _.defaults( { __related: oldRelated }, options ) ); | ||
} | ||
}); | ||
}, | ||
removeRelated: function( model, options ) { | ||
removeRelated: function( model, coll, options ) { | ||
if ( !this.related ) { | ||
@@ -811,10 +830,10 @@ return; | ||
this.setRelated( null ); | ||
this.onChange( this.instance, model, { _related: oldRelated } ); | ||
this.onChange( this.instance, model, _.defaults( { __related: oldRelated }, options ) ); | ||
} | ||
} | ||
}); | ||
Backbone.HasMany = Backbone.Relation.extend({ | ||
collectionType: null, | ||
options: { | ||
@@ -826,6 +845,5 @@ reverseRelation: { type: 'HasOne' }, | ||
}, | ||
initialize: function() { | ||
_.bindAll( this, 'onChange', 'handleAddition', 'handleRemoval', 'handleReset' ); | ||
this.instance.bind( 'relational:change:' + this.key, this.onChange ); | ||
initialize: function( opts ) { | ||
this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange ); | ||
@@ -838,21 +856,8 @@ // Handle a custom 'collectionType' | ||
if ( !this.collectionType.prototype instanceof Backbone.Collection ){ | ||
throw new Error( 'collectionType must inherit from Backbone.Collection' ); | ||
throw new Error( '`collectionType` must inherit from Backbone.Collection' ); | ||
} | ||
// Handle cases where a model/relation is created with a collection passed straight into 'attributes' | ||
if ( this.keyContents instanceof Backbone.Collection ) { | ||
this.setRelated( this._prepareCollection( this.keyContents ) ); | ||
} | ||
else { | ||
this.setRelated( this._prepareCollection() ); | ||
} | ||
this.findRelated( { silent: true } ); | ||
var related = this.findRelated( opts ); | ||
this.setRelated( related ); | ||
}, | ||
_getCollectionOptions: function() { | ||
return _.isFunction( this.options.collectionOptions ) ? | ||
this.options.collectionOptions( this.instance ) : | ||
this.options.collectionOptions; | ||
}, | ||
@@ -863,13 +868,14 @@ /** | ||
* @param {Backbone.Collection} [collection] | ||
* @return {Backbone.Collection} | ||
*/ | ||
_prepareCollection: function( collection ) { | ||
if ( this.related ) { | ||
this.related | ||
.unbind( 'relational:add', this.handleAddition ) | ||
.unbind( 'relational:remove', this.handleRemoval ) | ||
.unbind( 'relational:reset', this.handleReset ) | ||
this.stopListening( this.related ); | ||
} | ||
if ( !collection || !( collection instanceof Backbone.Collection ) ) { | ||
collection = new this.collectionType( [], this._getCollectionOptions() ); | ||
var options = _.isFunction( this.options.collectionOptions ) ? | ||
this.options.collectionOptions( this.instance ) : this.options.collectionOptions; | ||
collection = new this.collectionType( null, options ); | ||
} | ||
@@ -891,144 +897,120 @@ | ||
} | ||
this.listenTo( collection, 'relational:add', this.handleAddition ) | ||
.listenTo( collection, 'relational:remove', this.handleRemoval ) | ||
.listenTo( collection, 'relational:reset', this.handleReset ); | ||
collection | ||
.bind( 'relational:add', this.handleAddition ) | ||
.bind( 'relational:remove', this.handleRemoval ) | ||
.bind( 'relational:reset', this.handleReset ); | ||
return collection; | ||
}, | ||
/** | ||
* Find related Models. | ||
* @param {Object} [options] | ||
* @return {Backbone.Collection} | ||
*/ | ||
findRelated: function( options ) { | ||
if ( this.keyContents ) { | ||
var models = []; | ||
var related = null; | ||
if ( this.keyContents instanceof Backbone.Collection ) { | ||
models = this.keyContents.models; | ||
options = _.defaults( { parse: this.options.parse }, options ); | ||
// Replace 'this.related' by 'this.keyContents' if it is a Backbone.Collection | ||
if ( this.keyContents instanceof Backbone.Collection ) { | ||
this._prepareCollection( this.keyContents ); | ||
related = this.keyContents; | ||
} | ||
// Otherwise, 'this.keyContents' should be an array of related object ids. | ||
// Re-use the current 'this.related' if it is a Backbone.Collection; otherwise, create a new collection. | ||
else { | ||
var toAdd = []; | ||
_.each( this.keyContents, function( attributes ) { | ||
if ( attributes instanceof this.relatedModel ) { | ||
var model = attributes; | ||
} | ||
else { | ||
// If `merge` is true, update models here, instead of during update. | ||
model = this.relatedModel.findOrCreate( attributes, _.extend( { merge: true }, options, { create: this.options.createModels } ) ); | ||
} | ||
model && toAdd.push( model ); | ||
}, this ); | ||
if ( this.related instanceof Backbone.Collection ) { | ||
related = this.related; | ||
} | ||
else { | ||
// Handle cases the an API/user supplies just an Object/id instead of an Array | ||
this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ]; | ||
related = this._prepareCollection(); | ||
} | ||
// Try to find instances of the appropriate 'relatedModel' in the store | ||
_.each( this.keyContents || [], function( item ) { | ||
var model = null; | ||
if ( item instanceof this.relatedModel ) { | ||
model = item; | ||
} | ||
else if ( item || item === 0 ) { // since 0 can be a valid `id` as well | ||
model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } ); | ||
} | ||
related.update( toAdd, _.defaults( { merge: false, parse: false }, options ) ); | ||
} | ||
if ( model && !this.related.get( model ) ) { | ||
models.push( model ); | ||
} | ||
}, this ); | ||
} | ||
return related; | ||
}, | ||
// Add all found 'models' in on go, so 'add' will only be called once (and thus 'sort', etc.) | ||
if ( models.length ) { | ||
options = this.unsanitizeOptions( options ); | ||
this.related.add( models, options ); | ||
} | ||
/** | ||
* Normalize and reduce `keyContents` to a list of `ids`, for easier comparison | ||
* @param {String|Number|String[]|Number[]|Backbone.Collection} keyContents | ||
*/ | ||
setKeyContents: function( keyContents ) { | ||
this.keyContents = keyContents instanceof Backbone.Collection ? keyContents : null; | ||
this.keyIds = []; | ||
if ( !this.keyContents && ( keyContents || keyContents === 0 ) ) { // since 0 can be a valid `id` as well | ||
// Handle cases the an API/user supplies just an Object/id instead of an Array | ||
this.keyContents = _.isArray( keyContents ) ? keyContents : [ keyContents ]; | ||
_.each( this.keyContents, function( item ) { | ||
var itemId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item ); | ||
if ( itemId || itemId === 0 ) { | ||
this.keyIds.push( itemId ); | ||
} | ||
}, this ); | ||
} | ||
}, | ||
/** | ||
* If the key is changed, notify old & new reverse relations and initialize the new relation | ||
* Event handler for `change:<key>`. | ||
* If the contents of the key are changed, notify old & new reverse relations and initialize the new relation. | ||
*/ | ||
onChange: function( model, attr, options ) { | ||
options = this.sanitizeOptions( options ); | ||
this.keyContents = attr; | ||
// Replace 'this.related' by 'attr' if it is a Backbone.Collection | ||
if ( attr instanceof Backbone.Collection ) { | ||
this._prepareCollection( attr ); | ||
this.related = attr; | ||
} | ||
// Otherwise, 'attr' should be an array of related object ids. | ||
// Re-use the current 'this.related' if it is a Backbone.Collection, and remove any current entries. | ||
// Otherwise, create a new collection. | ||
else { | ||
var oldIds = {}, newIds = {}; | ||
options = options ? _.clone( options ) : {}; | ||
this.setKeyContents( attr ); | ||
this.changed = false; | ||
if (!_.isArray( attr ) && attr !== undefined) { | ||
attr = [ attr ]; | ||
} | ||
var related = this.findRelated( options ); | ||
this.setRelated( related ); | ||
_.each( attr, function( attributes ) { | ||
newIds[ attributes.id ] = true; | ||
if ( !options.silent ) { | ||
var dit = this; | ||
Backbone.Relational.eventQueue.add( function() { | ||
// The `changed` flag can be set in `handleAddition` or `handleRemoval` | ||
if ( dit.changed ) { | ||
dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true ); | ||
dit.changed = false; | ||
} | ||
}); | ||
var coll = this.related; | ||
if ( coll instanceof Backbone.Collection ) { | ||
// Make sure to operate on a copy since we're removing while iterating | ||
_.each( coll.models.slice(0) , function( model ) { | ||
// When fetch is called with the 'keepNewModels' option, we don't want to remove | ||
// client-created new models when the fetch is completed. | ||
if ( !options.keepNewModels || !model.isNew() ) { | ||
oldIds[ model.id ] = true; | ||
coll.remove( model, { silent: (model.id in newIds) } ); | ||
} | ||
}); | ||
} else { | ||
coll = this._prepareCollection(); | ||
} | ||
_.each( attr, function( attributes ) { | ||
var model = this.relatedModel.findOrCreate( attributes, { create: this.options.createModels } ); | ||
if (model) { | ||
coll.add( model, { silent: (attributes.id in oldIds)} ); | ||
} | ||
}, this); | ||
this.setRelated( coll ); | ||
} | ||
var dit = this; | ||
Backbone.Relational.eventQueue.add( function() { | ||
!options.silentChange && dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options ); | ||
}); | ||
}, | ||
tryAddRelated: function( model, options ) { | ||
options = this.sanitizeOptions( options ); | ||
if ( !this.related.get( model ) ) { | ||
// Check if this new model was specified in 'this.keyContents' | ||
var item = _.any( this.keyContents || [], function( item ) { | ||
var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item ); | ||
return !_.isNull( id ) && id === model.id; | ||
}, this ); | ||
if ( item ) { | ||
this.related.add( model, options ); | ||
} | ||
} | ||
}, | ||
/** | ||
* When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations. | ||
* (should be 'HasOne', must set 'this.instance' as their related). | ||
*/ | ||
*/ | ||
handleAddition: function( model, coll, options ) { | ||
//console.debug('handleAddition called; args=%o', arguments); | ||
// Make sure the model is in fact a valid model before continuing. | ||
// (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel) | ||
if ( !( model instanceof Backbone.Model ) ) { | ||
return; | ||
} | ||
options = options ? _.clone( options ) : {}; | ||
this.changed = true; | ||
options = this.sanitizeOptions( options ); | ||
_.each( this.getReverseRelations( model ) || [], function( relation ) { | ||
relation.addRelated( this.instance, options ); | ||
}, this ); | ||
_.each( this.getReverseRelations( model ), function( relation ) { | ||
relation.addRelated( this.instance, options ); | ||
}, this ); | ||
// Only trigger 'add' once the newly added model is initialized (so, has it's relations set up) | ||
// Only trigger 'add' once the newly added model is initialized (so, has its relations set up) | ||
var dit = this; | ||
Backbone.Relational.eventQueue.add( function() { | ||
!options.silentChange && dit.instance.trigger( 'add:' + dit.key, model, dit.related, options ); | ||
!options.silent && Backbone.Relational.eventQueue.add( function() { | ||
dit.instance.trigger( 'add:' + dit.key, model, dit.related, options ); | ||
}); | ||
}, | ||
/** | ||
@@ -1040,15 +1022,12 @@ * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations. | ||
//console.debug('handleRemoval called; args=%o', arguments); | ||
if ( !( model instanceof Backbone.Model ) ) { | ||
return; | ||
} | ||
options = this.sanitizeOptions( options ); | ||
options = options ? _.clone( options ) : {}; | ||
this.changed = true; | ||
_.each( this.getReverseRelations( model ) || [], function( relation ) { | ||
relation.removeRelated( this.instance, options ); | ||
}, this ); | ||
_.each( this.getReverseRelations( model ), function( relation ) { | ||
relation.removeRelated( this.instance, null, options ); | ||
}, this ); | ||
var dit = this; | ||
Backbone.Relational.eventQueue.add( function() { | ||
!options.silentChange && dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options ); | ||
!options.silent && Backbone.Relational.eventQueue.add( function() { | ||
dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options ); | ||
}); | ||
@@ -1058,14 +1037,23 @@ }, | ||
handleReset: function( coll, options ) { | ||
options = this.sanitizeOptions( options ); | ||
var dit = this; | ||
Backbone.Relational.eventQueue.add( function() { | ||
!options.silentChange && dit.instance.trigger( 'reset:' + dit.key, dit.related, options ); | ||
options = options ? _.clone( options ) : {}; | ||
!options.silent && Backbone.Relational.eventQueue.add( function() { | ||
dit.instance.trigger( 'reset:' + dit.key, dit.related, options ); | ||
}); | ||
}, | ||
tryAddRelated: function( model, coll, options ) { | ||
var item = _.contains( this.keyIds, model.id ); | ||
if ( item ) { | ||
this.addRelated( model, options ); | ||
this.keyIds = _.without( this.keyIds, model.id ); | ||
} | ||
}, | ||
addRelated: function( model, options ) { | ||
// Allow 'model' to set up its relations before proceeding. | ||
// (which can result in a call to 'addRelated' from a relation of 'model') | ||
var dit = this; | ||
options = this.unsanitizeOptions( options ); | ||
model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice | ||
model.queue( function() { | ||
if ( dit.related && !dit.related.get( model ) ) { | ||
@@ -1076,5 +1064,4 @@ dit.related.add( model, options ); | ||
}, | ||
removeRelated: function( model, options ) { | ||
options = this.unsanitizeOptions( options ); | ||
removeRelated: function( model, coll, options ) { | ||
if ( this.related.get( model ) ) { | ||
@@ -1085,3 +1072,3 @@ this.related.remove( model, options ); | ||
}); | ||
/** | ||
@@ -1092,3 +1079,3 @@ * A type of Backbone.Model that also maintains relations to other models and collections. | ||
* - 'remove:<key>' (model, related collection, options) | ||
* - 'update:<key>' (model, related model or collection, options) | ||
* - 'change:<key>' (model, related model or collection, options) | ||
*/ | ||
@@ -1101,18 +1088,20 @@ Backbone.RelationalModel = Backbone.Model.extend({ | ||
_queue: null, | ||
subModelTypeAttribute: 'type', | ||
subModelTypes: null, | ||
constructor: function( attributes, options ) { | ||
// Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'. | ||
// Defer 'processQueue', so that when 'Relation.createModels' is used we: | ||
// a) Survive 'Backbone.Collection.add'; this takes care we won't error on "can't add model to a set twice" | ||
// (by creating a model from properties, having the model add itself to the collection via one of | ||
// it's relations, then trying to add it to the collection). | ||
// b) Trigger 'HasMany' collection events only after the model is really fully set up. | ||
// Example that triggers both a and b: "p.get('jobs').add( { company: c, person: p } )". | ||
var dit = this; | ||
// Defer 'processQueue', so that when 'Relation.createModels' is used we trigger 'HasMany' | ||
// collection events only after the model is really fully set up. | ||
// Example: "p.get('jobs').add( { company: c, person: p } )". | ||
if ( options && options.collection ) { | ||
var dit = this, | ||
collection = this.collection = options.collection; | ||
// Prevent this option from cascading down to related models; they shouldn't go into this `if` clause. | ||
delete options.collection; | ||
this._deferProcessing = true; | ||
var processQueue = function( model ) { | ||
@@ -1122,8 +1111,8 @@ if ( model === dit ) { | ||
dit.processQueue(); | ||
options.collection.unbind( 'relational:add', processQueue ); | ||
collection.off( 'relational:add', processQueue ); | ||
} | ||
}; | ||
options.collection.bind( 'relational:add', processQueue ); | ||
// So we do process the queue eventually, regardless of whether this model really gets added to 'options.collection'. | ||
collection.on( 'relational:add', processQueue ); | ||
// So we do process the queue eventually, regardless of whether this model actually gets added to 'options.collection'. | ||
_.defer( function() { | ||
@@ -1133,2 +1122,4 @@ processQueue( dit ); | ||
} | ||
Backbone.Relational.store.processOrphanRelations(); | ||
@@ -1138,9 +1129,12 @@ this._queue = new Backbone.BlockingQueue(); | ||
Backbone.Relational.eventQueue.block(); | ||
Backbone.Model.apply( this, arguments ); | ||
// Try to run the global queue holding external events | ||
Backbone.Relational.eventQueue.unblock(); | ||
try { | ||
Backbone.Model.apply( this, arguments ); | ||
} | ||
finally { | ||
// Try to run the global queue holding external events | ||
Backbone.Relational.eventQueue.unblock(); | ||
} | ||
}, | ||
/** | ||
@@ -1150,7 +1144,42 @@ * Override 'trigger' to queue 'change' and 'change:*' events | ||
trigger: function( eventName ) { | ||
if ( eventName.length > 5 && 'change' === eventName.substr( 0, 6 ) ) { | ||
var dit = this, args = arguments; | ||
if ( eventName.length > 5 && eventName.indexOf( 'change' ) === 0 ) { | ||
var dit = this, | ||
args = arguments; | ||
Backbone.Relational.eventQueue.add( function() { | ||
Backbone.Model.prototype.trigger.apply( dit, args ); | ||
}); | ||
if ( !dit._isInitialized ) { | ||
return; | ||
} | ||
// Determine if the `change` event is still valid, now that all relations are populated | ||
var changed = true; | ||
if ( eventName === 'change' ) { | ||
changed = dit.hasChanged(); | ||
} | ||
else { | ||
var attr = eventName.slice( 7 ), | ||
rel = dit.getRelation( attr ); | ||
if ( rel ) { | ||
// If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`. | ||
// These take precedence over `change:attr` events triggered by `Model.set`. | ||
// The relation set a fourth attribute to `true`. If this attribute is present, | ||
// continue triggering this event; otherwise, it's from `Model.set` and should be stopped. | ||
changed = ( args[ 4 ] === true ); | ||
// If this event was triggered by a relation, set the right value in `this.changed` | ||
// (a Collection or Model instead of raw data). | ||
if ( changed ) { | ||
dit.changed[ attr ] = args[ 2 ]; | ||
} | ||
// Otherwise, this event is from `Model.set`. If the relation doesn't report a change, | ||
// remove attr from `dit.changed` so `hasChanged` doesn't take it into account. | ||
else if ( !rel.changed ) { | ||
delete dit.changed[ attr ]; | ||
} | ||
} | ||
} | ||
changed && Backbone.Model.prototype.trigger.apply( dit, args ); | ||
}); | ||
} | ||
@@ -1163,3 +1192,3 @@ else { | ||
}, | ||
/** | ||
@@ -1169,15 +1198,9 @@ * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance. | ||
*/ | ||
initializeRelations: function() { | ||
initializeRelations: function( options ) { | ||
this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once | ||
this._relations = []; | ||
this._relations = {}; | ||
_.each( this.relations || [], function( rel ) { | ||
var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type ); | ||
if ( type && type.prototype instanceof Backbone.Relation ) { | ||
new type( this, rel ); // Also pushes the new Relation into _relations | ||
} | ||
else { | ||
Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid type!', rel ); | ||
} | ||
}, this ); | ||
Backbone.Relational.store.initializeRelation( this, rel, options ); | ||
}, this ); | ||
@@ -1195,3 +1218,3 @@ this._isInitialized = true; | ||
if ( this._isInitialized && !this.isLocked() ) { | ||
_.each( this._relations || [], function( rel ) { | ||
_.each( this._relations, function( rel ) { | ||
// Update from data in `rel.keySource` if set, or `rel.key` otherwise | ||
@@ -1205,3 +1228,3 @@ var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ]; | ||
}, | ||
/** | ||
@@ -1213,3 +1236,3 @@ * Either add to the queue (if we're not initialized yet), or execute right away. | ||
}, | ||
/** | ||
@@ -1223,3 +1246,3 @@ * Process _queue | ||
}, | ||
/** | ||
@@ -1231,9 +1254,5 @@ * Get a specific relation. | ||
getRelation: function( key ) { | ||
return _.detect( this._relations, function( rel ) { | ||
if ( rel.key === key ) { | ||
return true; | ||
} | ||
}, this ); | ||
return this._relations[ key ]; | ||
}, | ||
/** | ||
@@ -1244,5 +1263,5 @@ * Get all of the created relations. | ||
getRelations: function() { | ||
return this._relations; | ||
return _.values( this._relations ); | ||
}, | ||
/** | ||
@@ -1252,32 +1271,32 @@ * Retrieve related objects. | ||
* @param [options] {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'. | ||
* @param [update=false] {boolean} Whether to force a fetch from the server (updating existing models). | ||
* @param [refresh=false] {boolean} Fetch existing models from the server as well (in order to update them). | ||
* @return {jQuery.when[]} An array of request objects | ||
*/ | ||
fetchRelated: function( key, options, update ) { | ||
options || ( options = {} ); | ||
fetchRelated: function( key, options, refresh ) { | ||
// Set default `options` for fetch | ||
options = _.extend( { update: true, remove: false }, options ); | ||
var setUrl, | ||
requests = [], | ||
rel = this.getRelation( key ), | ||
keyContents = rel && rel.keyContents, | ||
toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) { | ||
var id = Backbone.Relational.store.resolveIdForItem( rel.relatedModel, item ); | ||
return !_.isNull( id ) && ( update || !Backbone.Relational.store.find( rel.relatedModel, id ) ); | ||
keys = rel && ( rel.keyIds || [ rel.keyId ] ), | ||
toFetch = keys && _.select( keys || [], function( id ) { | ||
return ( id || id === 0 ) && ( refresh || !Backbone.Relational.store.find( rel.relatedModel, id ) ); | ||
}, this ); | ||
if ( toFetch && toFetch.length ) { | ||
// Create a model for each entry in 'keyContents' that is to be fetched | ||
var models = _.map( toFetch, function( item ) { | ||
var model; | ||
// Find (or create) a model for each one that is to be fetched | ||
var created = [], | ||
models = _.map( toFetch, function( id ) { | ||
var model = Backbone.Relational.store.find( rel.relatedModel, id ); | ||
if ( !model ) { | ||
var attrs = {}; | ||
attrs[ rel.relatedModel.prototype.idAttribute ] = id; | ||
model = rel.relatedModel.findOrCreate( attrs, options ); | ||
created.push( model ); | ||
} | ||
if ( _.isObject( item ) ) { | ||
model = rel.relatedModel.findOrCreate( item ); | ||
} | ||
else { | ||
var attrs = {}; | ||
attrs[ rel.relatedModel.prototype.idAttribute ] = item; | ||
model = rel.relatedModel.findOrCreate( attrs ); | ||
} | ||
return model; | ||
}, this ); | ||
return model; | ||
}, this ); | ||
@@ -1297,11 +1316,10 @@ // Try if the 'collection' can provide a url to fetch a set of models in one request. | ||
var args = arguments; | ||
_.each( models || [], function( model ) { | ||
model.trigger( 'destroy', model, model.collection, options ); | ||
options.error && options.error.apply( model, args ); | ||
}); | ||
_.each( created, function( model ) { | ||
model.trigger( 'destroy', model, model.collection, options ); | ||
options.error && options.error.apply( model, args ); | ||
}); | ||
}, | ||
url: setUrl | ||
}, | ||
options, | ||
{ add: true } | ||
options | ||
); | ||
@@ -1312,8 +1330,10 @@ | ||
else { | ||
requests = _.map( models || [], function( model ) { | ||
requests = _.map( models, function( model ) { | ||
var opts = _.defaults( | ||
{ | ||
error: function() { | ||
model.trigger( 'destroy', model, model.collection, options ); | ||
options.error && options.error.apply( model, arguments ); | ||
if ( _.contains( created, model ) ) { | ||
model.trigger( 'destroy', model, model.collection, options ); | ||
options.error && options.error.apply( model, arguments ); | ||
} | ||
} | ||
@@ -1330,3 +1350,28 @@ }, | ||
}, | ||
get: function( attr ) { | ||
var originalResult = Backbone.Model.prototype.get.call( this, attr ); | ||
// Use `originalResult` get if dotNotation not enabled or not required because no dot is in `attr` | ||
if ( !this.dotNotation || attr.indexOf( '.' ) === -1 ) { | ||
return originalResult; | ||
} | ||
// Go through all splits and return the final result | ||
var splits = attr.split( '.' ); | ||
var result = _.reduce(splits, function( model, split ) { | ||
if ( !( model instanceof Backbone.Model ) ) { | ||
throw new Error( 'Attribute must be an instanceof Backbone.Model. Is: ' + model + ', currentSplit: ' + split ); | ||
} | ||
return Backbone.Model.prototype.get.call( model, split ); | ||
}, this ); | ||
if ( originalResult !== undefined && result !== undefined ) { | ||
throw new Error( "Ambiguous result for '" + attr + "'. direct result: " + originalResult + ", dotNotation: " + result ); | ||
} | ||
return originalResult || result; | ||
}, | ||
set: function( key, value, options ) { | ||
@@ -1349,24 +1394,27 @@ Backbone.Relational.eventQueue.block(); | ||
// Ideal place to set up relations :) | ||
if ( !this._isInitialized && !this.isLocked() ) { | ||
this.constructor.initializeModelHierarchy(); | ||
try { | ||
if ( !this._isInitialized && !this.isLocked() ) { | ||
this.constructor.initializeModelHierarchy(); | ||
Backbone.Relational.store.register( this ); | ||
Backbone.Relational.store.register( this ); | ||
this.initializeRelations(); | ||
this.initializeRelations( options ); | ||
} | ||
// Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true} | ||
else if ( attributes && this.idAttribute in attributes ) { | ||
Backbone.Relational.store.update( this ); | ||
} | ||
if ( attributes ) { | ||
this.updateRelations( options ); | ||
} | ||
} | ||
// Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true} | ||
else if ( attributes && this.idAttribute in attributes ) { | ||
Backbone.Relational.store.update( this ); | ||
finally { | ||
// Try to run the global queue holding external events | ||
Backbone.Relational.eventQueue.unblock(); | ||
} | ||
if ( attributes ) { | ||
this.updateRelations( options ); | ||
} | ||
// Try to run the global queue holding external events | ||
Backbone.Relational.eventQueue.unblock(); | ||
return result; | ||
}, | ||
unset: function( attribute, options ) { | ||
@@ -1383,3 +1431,3 @@ Backbone.Relational.eventQueue.block(); | ||
}, | ||
clear: function( options ) { | ||
@@ -1396,13 +1444,2 @@ Backbone.Relational.eventQueue.block(); | ||
}, | ||
/** | ||
* Override 'change', so the change will only execute after 'set' has finised (relations are updated), | ||
* and 'previousAttributes' will be available when the event is fired. | ||
*/ | ||
change: function( options ) { | ||
var dit = this, args = arguments; | ||
Backbone.Relational.eventQueue.add( function() { | ||
Backbone.Model.prototype.change.apply( dit, args ); | ||
}); | ||
}, | ||
@@ -1415,13 +1452,13 @@ clone: function() { | ||
_.each( this.getRelations() || [], function( rel ) { | ||
delete attributes[ rel.key ]; | ||
}); | ||
_.each( this.getRelations(), function( rel ) { | ||
delete attributes[ rel.key ]; | ||
}); | ||
return new this.constructor( attributes ); | ||
}, | ||
/** | ||
* Convert relations to JSON, omits them when required | ||
*/ | ||
toJSON: function(options) { | ||
toJSON: function( options ) { | ||
// If this Model has already been fully serialized in this branch once, return to avoid loops | ||
@@ -1439,55 +1476,55 @@ if ( this.isLocked() ) { | ||
_.each( this._relations || [], function( rel ) { | ||
var value = json[ rel.key ]; | ||
_.each( this._relations, function( rel ) { | ||
var value = json[ rel.key ]; | ||
if ( rel.options.includeInJSON === true) { | ||
if ( value && _.isFunction( value.toJSON ) ) { | ||
json[ rel.keyDestination ] = value.toJSON( options ); | ||
} | ||
else { | ||
json[ rel.keyDestination ] = null; | ||
} | ||
if ( rel.options.includeInJSON === true) { | ||
if ( value && _.isFunction( value.toJSON ) ) { | ||
json[ rel.keyDestination ] = value.toJSON( options ); | ||
} | ||
else if ( _.isString( rel.options.includeInJSON ) ) { | ||
if ( value instanceof Backbone.Collection ) { | ||
json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON ); | ||
} | ||
else if ( value instanceof Backbone.Model ) { | ||
json[ rel.keyDestination ] = value.get( rel.options.includeInJSON ); | ||
} | ||
else { | ||
json[ rel.keyDestination ] = null; | ||
} | ||
else { | ||
json[ rel.keyDestination ] = null; | ||
} | ||
else if ( _.isArray( rel.options.includeInJSON ) ) { | ||
if ( value instanceof Backbone.Collection ) { | ||
var valueSub = []; | ||
value.each( function( model ) { | ||
var curJson = {}; | ||
_.each( rel.options.includeInJSON, function( key ) { | ||
curJson[ key ] = model.get( key ); | ||
}); | ||
valueSub.push( curJson ); | ||
}); | ||
json[ rel.keyDestination ] = valueSub; | ||
} | ||
else if ( value instanceof Backbone.Model ) { | ||
var valueSub = {}; | ||
} | ||
else if ( _.isString( rel.options.includeInJSON ) ) { | ||
if ( value instanceof Backbone.Collection ) { | ||
json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON ); | ||
} | ||
else if ( value instanceof Backbone.Model ) { | ||
json[ rel.keyDestination ] = value.get( rel.options.includeInJSON ); | ||
} | ||
else { | ||
json[ rel.keyDestination ] = null; | ||
} | ||
} | ||
else if ( _.isArray( rel.options.includeInJSON ) ) { | ||
if ( value instanceof Backbone.Collection ) { | ||
var valueSub = []; | ||
value.each( function( model ) { | ||
var curJson = {}; | ||
_.each( rel.options.includeInJSON, function( key ) { | ||
valueSub[ key ] = value.get( key ); | ||
curJson[ key ] = model.get( key ); | ||
}); | ||
json[ rel.keyDestination ] = valueSub; | ||
} | ||
else { | ||
json[ rel.keyDestination ] = null; | ||
} | ||
valueSub.push( curJson ); | ||
}); | ||
json[ rel.keyDestination ] = valueSub; | ||
} | ||
else if ( value instanceof Backbone.Model ) { | ||
var valueSub = {}; | ||
_.each( rel.options.includeInJSON, function( key ) { | ||
valueSub[ key ] = value.get( key ); | ||
}); | ||
json[ rel.keyDestination ] = valueSub; | ||
} | ||
else { | ||
delete json[ rel.key ]; | ||
json[ rel.keyDestination ] = null; | ||
} | ||
} | ||
else { | ||
delete json[ rel.key ]; | ||
} | ||
if ( rel.keyDestination !== rel.key ) { | ||
delete json[ rel.key ]; | ||
} | ||
}); | ||
if ( rel.keyDestination !== rel.key ) { | ||
delete json[ rel.key ]; | ||
} | ||
}); | ||
@@ -1499,2 +1536,7 @@ this.release(); | ||
{ | ||
/** | ||
* | ||
* @param superModel | ||
* @returns {Backbone.RelationalModel.constructor} | ||
*/ | ||
setup: function( superModel ) { | ||
@@ -1519,25 +1561,30 @@ // We don't want to share a relations array with a parent, as this will cause problems with | ||
_.each( this.prototype.relations || [], function( rel ) { | ||
if ( !rel.model ) { | ||
rel.model = this; | ||
if ( !rel.model ) { | ||
rel.model = this; | ||
} | ||
if ( rel.reverseRelation && rel.model === this ) { | ||
var preInitialize = true; | ||
if ( _.isString( rel.relatedModel ) ) { | ||
/** | ||
* The related model might not be defined for two reasons | ||
* 1. it is related to itself | ||
* 2. it never gets defined, e.g. a typo | ||
* 3. the model hasn't been defined yet, but will be later | ||
* In neither of these cases do we need to pre-initialize reverse relations. | ||
* However, for 3. (which is, to us, indistinguishable from 2.), we do need to attempt | ||
* setting up this relation again later, in case the related model is defined later. | ||
*/ | ||
var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel ); | ||
preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel ); | ||
} | ||
if ( rel.reverseRelation && rel.model === this ) { | ||
var preInitialize = true; | ||
if ( _.isString( rel.relatedModel ) ) { | ||
/** | ||
* The related model might not be defined for two reasons | ||
* 1. it never gets defined, e.g. a typo | ||
* 2. it is related to itself | ||
* In neither of these cases do we need to pre-initialize reverse relations. | ||
*/ | ||
var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel ); | ||
preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel ); | ||
} | ||
var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type ); | ||
if ( preInitialize && type && type.prototype instanceof Backbone.Relation ) { | ||
new type( null, rel ); | ||
} | ||
if ( preInitialize ) { | ||
Backbone.Relational.store.initializeRelation( null, rel ); | ||
} | ||
}, this ); | ||
else if ( _.isString( rel.relatedModel ) ) { | ||
Backbone.Relational.store.addOrphanRelation( rel ); | ||
} | ||
} | ||
}, this ); | ||
@@ -1572,2 +1619,5 @@ return this; | ||
/** | ||
* | ||
*/ | ||
initializeModelHierarchy: function() { | ||
@@ -1611,3 +1661,3 @@ // If we're here for the first time, try to determine if this modelType has a 'superModel'. | ||
* - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found. | ||
* - If `attributes` is an object, the model will be updated with `attributes` if found. | ||
* - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`. | ||
* Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`). | ||
@@ -1617,16 +1667,21 @@ * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model. | ||
* @param {Boolean} [options.create=true] | ||
* @param {Boolean} [options.merge=true] | ||
* @param {Boolean} [options.parse=false] | ||
* @return {Backbone.RelationalModel} | ||
*/ | ||
findOrCreate: function( attributes, options ) { | ||
var parsedAttributes = (_.isObject( attributes ) && this.prototype.parse) ? this.prototype.parse( attributes ) : attributes; | ||
options || ( options = {} ); | ||
var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ? | ||
this.prototype.parse( attributes ) : attributes; | ||
// Try to find an instance of 'this' model type in the store | ||
var model = Backbone.Relational.store.find( this, parsedAttributes ); | ||
// If we found an instance, update it with the data in 'item'; if not, create an instance | ||
// (unless 'options.create' is false). | ||
// If we found an instance, update it with the data in 'item' (unless 'options.merge' is false). | ||
// If not, create an instance (unless 'options.create' is false). | ||
if ( _.isObject( attributes ) ) { | ||
if ( model ) { | ||
if ( model && options.merge !== false ) { | ||
model.set( parsedAttributes, options ); | ||
} | ||
else if ( !options || ( options && options.create !== false ) ) { | ||
else if ( !model && options.create !== false ) { | ||
model = this.build( attributes, options ); | ||
@@ -1640,6 +1695,8 @@ } | ||
_.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore ); | ||
/** | ||
* Override Backbone.Collection._prepareModel, so objects will be built using the correct type | ||
* if the collection.model has subModels. | ||
* Attempts to find a model for `attrs` in Backbone.store through `findOrCreate` | ||
* (which sets the new properties on it if found), or instantiates a new model. | ||
*/ | ||
@@ -1667,3 +1724,4 @@ Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel; | ||
if ( !model._validate( attrs, options ) ) { | ||
if ( model && model.isNew() && !model._validate( attrs, options ) ) { | ||
this.trigger( 'invalid', this, attrs, options ); | ||
model = false; | ||
@@ -1676,41 +1734,54 @@ } | ||
/** | ||
* Override Backbone.Collection.add, so objects fetched from the server multiple times will | ||
* update the existing Model. Also, trigger 'relational:add'. | ||
* Override Backbone.Collection.add, so we'll create objects from attributes where required, | ||
* and update the existing models. Also, trigger 'relational:add'. | ||
*/ | ||
var add = Backbone.Collection.prototype.__add = Backbone.Collection.prototype.add; | ||
Backbone.Collection.prototype.add = function( models, options ) { | ||
options || (options = {}); | ||
if ( !_.isArray( models ) ) { | ||
models = [ models ]; | ||
// Short-circuit if this Collection doesn't hold RelationalModels | ||
if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) { | ||
return add.apply( this, arguments ); | ||
} | ||
var modelsToAdd = []; | ||
models = _.isArray( models ) ? models.slice() : [ models ]; | ||
// Set default options to the same values as `add` uses, so `findOrCreate` will also respect those. | ||
options = _.extend( { merge: false }, options ); | ||
var newModels = [], | ||
toAdd = []; | ||
//console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options ); | ||
_.each( models || [], function( model ) { | ||
_.each( models, function( model ) { | ||
if ( !( model instanceof Backbone.Model ) ) { | ||
// `_prepareModel` attempts to find `model` in Backbone.store through `findOrCreate`, | ||
// and sets the new properties on it if is found. Otherwise, a new model is instantiated. | ||
model = Backbone.Collection.prototype._prepareModel.call( this, model, options ); | ||
} | ||
if ( model instanceof Backbone.Model && !this.get( model ) ) { | ||
modelsToAdd.push( model ); | ||
if ( model ) { | ||
toAdd.push( model ); | ||
if ( !( this.get( model ) || this.get( model.cid ) ) ) { | ||
newModels.push( model ); | ||
} | ||
}, this ); | ||
// If we arrive in `add` while performing a `set` (after a create, so the model gains an `id`), | ||
// we may get here before `_onModelEvent` has had the chance to update `_byId`. | ||
else if ( model.id != null ) { | ||
this._byId[ model.id ] = model; | ||
} | ||
} | ||
}, this ); | ||
// Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc). | ||
if ( modelsToAdd.length ) { | ||
add.call( this, modelsToAdd, options ); | ||
add.call( this, toAdd, options ); | ||
_.each( modelsToAdd || [], function( model ) { | ||
_.each( newModels, function( model ) { | ||
// Fire a `relational:add` event for any model in `newModels` that has actually been added to the collection. | ||
if ( this.get( model ) || this.get( model.cid ) ) { | ||
this.trigger( 'relational:add', model, this, options ); | ||
}, this ); | ||
} | ||
} | ||
}, this ); | ||
return this; | ||
}; | ||
/** | ||
@@ -1721,19 +1792,25 @@ * Override 'Backbone.Collection.remove' to trigger 'relational:remove'. | ||
Backbone.Collection.prototype.remove = function( models, options ) { | ||
options || (options = {}); | ||
if ( !_.isArray( models ) ) { | ||
models = [ models ]; | ||
// Short-circuit if this Collection doesn't hold RelationalModels | ||
if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) { | ||
return remove.apply( this, arguments ); | ||
} | ||
else { | ||
models = models.slice( 0 ); | ||
} | ||
models = _.isArray( models ) ? models.slice() : [ models ]; | ||
options || ( options = {} ); | ||
var toRemove = []; | ||
//console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options ); | ||
_.each( models || [], function( model ) { | ||
model = this.get( model ); | ||
_.each( models, function( model ) { | ||
model = this.get( model ) || this.get( model.cid ); | ||
model && toRemove.push( model ); | ||
}, this ); | ||
if ( model instanceof Backbone.Model ) { | ||
remove.call( this, model, options ); | ||
this.trigger('relational:remove', model, this, options); | ||
} | ||
if ( toRemove.length ) { | ||
remove.call( this, toRemove, options ); | ||
_.each( toRemove, function( model ) { | ||
this.trigger('relational:remove', model, this, options); | ||
}, this ); | ||
} | ||
@@ -1749,4 +1826,7 @@ return this; | ||
reset.call( this, models, options ); | ||
this.trigger( 'relational:reset', this, options ); | ||
if ( this.model.prototype instanceof Backbone.RelationalModel ) { | ||
this.trigger( 'relational:reset', this, options ); | ||
} | ||
return this; | ||
@@ -1761,7 +1841,10 @@ }; | ||
sort.call( this, options ); | ||
this.trigger( 'relational:reset', this, options ); | ||
if ( this.model.prototype instanceof Backbone.RelationalModel ) { | ||
this.trigger( 'relational:reset', this, options ); | ||
} | ||
return this; | ||
}; | ||
/** | ||
@@ -1773,17 +1856,21 @@ * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations | ||
Backbone.Collection.prototype.trigger = function( eventName ) { | ||
// Short-circuit if this Collection doesn't hold RelationalModels | ||
if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) { | ||
return trigger.apply( this, arguments ); | ||
} | ||
if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) { | ||
var dit = this, args = arguments; | ||
var dit = this, | ||
args = arguments; | ||
if (eventName === 'add') { | ||
if ( _.isObject( args[ 3 ] ) ) { | ||
args = _.toArray( args ); | ||
// the fourth argument in case of a regular add is the option object. | ||
// the fourth argument is the option object. | ||
// we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked | ||
if (_.isObject( args[3] ) ) { | ||
args[3] = _.clone( args[3] ); | ||
} | ||
args[ 3 ] = _.clone( args[ 3 ] ); | ||
} | ||
Backbone.Relational.eventQueue.add( function() { | ||
trigger.apply( dit, args ); | ||
}); | ||
trigger.apply( dit, args ); | ||
}); | ||
} | ||
@@ -1790,0 +1877,0 @@ else { |
/** | ||
* Backbone-relational.js 0.7.0 | ||
* (c) 2011-2013 Paul Uithol | ||
* Backbone-relational.js 0.8.0 | ||
* (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors) | ||
* | ||
@@ -9,2 +9,2 @@ * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt. | ||
*/ | ||
(function(a){"use strict";var b,c,d;"undefined"==typeof window?(b=require("underscore"),c=require("backbone"),d=module.exports=c):(b=window._,c=window.Backbone,d=window),c.Relational={showWarnings:!0},c.Semaphore={_permitsAvailable:null,_permitsUsed:0,acquire:function(){if(this._permitsAvailable&&this._permitsUsed>=this._permitsAvailable)throw Error("Max permits acquired");this._permitsUsed++},release:function(){if(0===this._permitsUsed)throw Error("All permits released");this._permitsUsed--},isLocked:function(){return this._permitsUsed>0},setAvailablePermits:function(a){if(this._permitsUsed>a)throw Error("Available permits cannot be less than used permits");this._permitsAvailable=a}},c.BlockingQueue=function(){this._queue=[]},b.extend(c.BlockingQueue.prototype,c.Semaphore,{_queue:null,add:function(a){this.isBlocked()?this._queue.push(a):a()},process:function(){for(;this._queue&&this._queue.length;)this._queue.shift()()},block:function(){this.acquire()},unblock:function(){this.release(),this.isBlocked()||this.process()},isBlocked:function(){return this.isLocked()}}),c.Relational.eventQueue=new c.BlockingQueue,c.Store=function(){this._collections=[],this._reverseRelations=[],this._subModels=[],this._modelScopes=[d]},b.extend(c.Store.prototype,c.Events,{addModelScope:function(a){this._modelScopes.push(a)},addSubModels:function(a,b){this._subModels.push({superModelType:b,subModels:a})},setupSuperModel:function(c){b.find(this._subModels||[],function(d){return b.find(d.subModels||[],function(b,e){var f=this.getObjectByName(b);return c===f?(d.superModelType._subModels[e]=c,c._superModel=d.superModelType,c._subModelTypeValue=e,c._subModelTypeAttribute=d.superModelType.prototype.subModelTypeAttribute,!0):a},this)},this)},addReverseRelation:function(a){var c=b.any(this._reverseRelations||[],function(c){return b.all(a||[],function(a,b){return a===c[b]})});if(!c&&a.model&&a.type){this._reverseRelations.push(a);var d=function(a,c){a.prototype.relations||(a.prototype.relations=[]),a.prototype.relations.push(c),b.each(a._subModels||[],function(a){d(a,c)},this)};d(a.model,a),this.retroFitRelation(a)}},retroFitRelation:function(a){var b=this.getCollection(a.model);b.each(function(b){b instanceof a.model&&new a.type(b,a)},this)},getCollection:function(a){a instanceof c.RelationalModel&&(a=a.constructor);for(var d=a;d._superModel;)d=d._superModel;var e=b.detect(this._collections,function(a){return a.model===d});return e||(e=this._createCollection(d)),e},getObjectByName:function(c){var d=c.split("."),e=null;return b.find(this._modelScopes||[],function(c){return e=b.reduce(d||[],function(b,c){return b?b[c]:a},c),e&&e!==c?!0:a},this),e},_createCollection:function(a){var b;return a instanceof c.RelationalModel&&(a=a.constructor),a.prototype instanceof c.RelationalModel&&(b=new c.Collection,b.model=a,this._collections.push(b)),b},resolveIdForItem:function(a,d){var e=b.isString(d)||b.isNumber(d)?d:null;return null===e&&(d instanceof c.RelationalModel?e=d.id:b.isObject(d)&&(e=d[a.prototype.idAttribute])),e||0===e||(e=null),e},find:function(a,b){var c=this.resolveIdForItem(a,b),d=this.getCollection(a);if(d){var e=d.get(c);if(e instanceof a)return e}return null},register:function(a){var b=this.getCollection(a);if(b){if(b.get(a))throw Error("Cannot instantiate more than one Backbone.RelationalModel with the same id per type!");var c=a.collection;b.add(a),a.bind("destroy",this.unregister,this),a.collection=c}},update:function(a){var b=this.getCollection(a);b._onModelEvent("change:"+a.idAttribute,a,b)},unregister:function(a){a.unbind("destroy",this.unregister);var b=this.getCollection(a);b&&b.remove(a)}}),c.Relational.store=new c.Store,c.Relation=function(a,d){if(this.instance=a,d=b.isObject(d)?d:{},this.reverseRelation=b.defaults(d.reverseRelation||{},this.options.reverseRelation),this.reverseRelation.type=b.isString(this.reverseRelation.type)?c[this.reverseRelation.type]||c.Relational.store.getObjectByName(this.reverseRelation.type):this.reverseRelation.type,this.model=d.model||this.instance.constructor,this.options=b.defaults(d,this.options,c.Relation.prototype.options),this.key=this.options.key,this.keySource=this.options.keySource||this.key,this.keyDestination=this.options.keyDestination||this.keySource||this.key,this.relatedModel=this.options.relatedModel,b.isString(this.relatedModel)&&(this.relatedModel=c.Relational.store.getObjectByName(this.relatedModel)),this.checkPreconditions()){if(a){var e=this.keySource;e!==this.key&&"object"==typeof this.instance.get(this.key)&&(e=this.key),this.keyContents=this.instance.get(e),this.keySource!==this.key&&this.instance.unset(this.keySource,{silent:!0}),this.instance._relations.push(this)}!this.options.isAutoRelation&&this.reverseRelation.type&&this.reverseRelation.key&&c.Relational.store.addReverseRelation(b.defaults({isAutoRelation:!0,model:this.relatedModel,relatedModel:this.model,reverseRelation:this.options},this.reverseRelation)),b.bindAll(this,"_modelRemovedFromCollection","_relatedModelAdded","_relatedModelRemoved"),a&&(this.initialize(),d.autoFetch&&this.instance.fetchRelated(d.key,b.isObject(d.autoFetch)?d.autoFetch:{}),c.Relational.store.getCollection(this.instance).bind("relational:remove",this._modelRemovedFromCollection),c.Relational.store.getCollection(this.relatedModel).bind("relational:add",this._relatedModelAdded).bind("relational:remove",this._relatedModelRemoved))}},c.Relation.extend=c.Model.extend,b.extend(c.Relation.prototype,c.Events,c.Semaphore,{options:{createModels:!0,includeInJSON:!0,isAutoRelation:!1,autoFetch:!1},instance:null,key:null,keyContents:null,relatedModel:null,reverseRelation:null,related:null,_relatedModelAdded:function(a,b,c){var d=this;a.queue(function(){d.tryAddRelated(a,c)})},_relatedModelRemoved:function(a,b,c){this.removeRelated(a,c)},_modelRemovedFromCollection:function(a){a===this.instance&&this.destroy()},checkPreconditions:function(){var a=this.instance,d=this.key,e=this.model,f=this.relatedModel,g=c.Relational.showWarnings&&"undefined"!=typeof console;if(!e||!d||!f)return g&&console.warn("Relation=%o; no model, key or relatedModel (%o, %o, %o)",this,e,d,f),!1;if(!(e.prototype instanceof c.RelationalModel))return g&&console.warn("Relation=%o; model does not inherit from Backbone.RelationalModel (%o)",this,a),!1;if(!(f.prototype instanceof c.RelationalModel))return g&&console.warn("Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)",this,f),!1;if(this instanceof c.HasMany&&this.reverseRelation.type===c.HasMany)return g&&console.warn("Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.",this),!1;if(a&&a._relations.length){var h=b.any(a._relations||[],function(a){var b=this.reverseRelation.key&&a.reverseRelation.key;return a.relatedModel===f&&a.key===d&&(!b||this.reverseRelation.key===a.reverseRelation.key)},this);if(h)return g&&console.warn("Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists",this,a,d,f,this.reverseRelation.key),!1}return!0},setRelated:function(a,c){this.related=a,this.instance.acquire(),this.instance.set(this.key,a,b.defaults(c||{},{silent:!0})),this.instance.release()},_isReverseRelation:function(a){return a.instance instanceof this.relatedModel&&this.reverseRelation.key===a.key&&this.key===a.reverseRelation.key?!0:!1},getReverseRelations:function(a){var c=[],d=b.isUndefined(a)?this.related&&(this.related.models||[this.related]):[a];return b.each(d||[],function(a){b.each(a.getRelations()||[],function(a){this._isReverseRelation(a)&&c.push(a)},this)},this),c},sanitizeOptions:function(a){return a=a?b.clone(a):{},a.silent&&(a.silentChange=!0,delete a.silent),a},unsanitizeOptions:function(a){return a=a?b.clone(a):{},a.silentChange&&(a.silent=!0,delete a.silentChange),a},destroy:function(){c.Relational.store.getCollection(this.instance).unbind("relational:remove",this._modelRemovedFromCollection),c.Relational.store.getCollection(this.relatedModel).unbind("relational:add",this._relatedModelAdded).unbind("relational:remove",this._relatedModelRemoved),b.each(this.getReverseRelations()||[],function(a){a.removeRelated(this.instance)},this)}}),c.HasOne=c.Relation.extend({options:{reverseRelation:{type:"HasMany"}},initialize:function(){b.bindAll(this,"onChange"),this.instance.bind("relational:change:"+this.key,this.onChange);var a=this.findRelated({silent:!0});this.setRelated(a),b.each(this.getReverseRelations()||[],function(a){a.addRelated(this.instance)},this)},findRelated:function(){var b=this.keyContents,c=null;return b instanceof this.relatedModel?c=b:(b||0===b)&&(c=this.relatedModel.findOrCreate(b,{create:this.options.createModels})),c},onChange:function(a,d,e){if(!this.isLocked()){this.acquire(),e=this.sanitizeOptions(e);var f=b.isUndefined(e._related),g=f?this.related:e._related;if(f)if(this.keyContents=d,d instanceof this.relatedModel)this.related=d;else if(d){var h=this.findRelated(e);this.setRelated(h)}else this.setRelated(null);if(g&&this.related!==g&&b.each(this.getReverseRelations(g)||[],function(a){a.removeRelated(this.instance,e)},this),b.each(this.getReverseRelations()||[],function(a){a.addRelated(this.instance,e)},this),!e.silentChange&&this.related!==g){var i=this;c.Relational.eventQueue.add(function(){i.instance.trigger("update:"+i.key,i.instance,i.related,e)})}this.release()}},tryAddRelated:function(a,d){if(!this.related){d=this.sanitizeOptions(d);var e=this.keyContents;if(e||0===e){var f=c.Relational.store.resolveIdForItem(this.relatedModel,e);b.isNull(f)||a.id!==f||this.addRelated(a,d)}}},addRelated:function(a){if(a!==this.related){var c=this.related||null;this.setRelated(a),this.onChange(this.instance,a,{_related:c})}},removeRelated:function(a){if(this.related&&a===this.related){var c=this.related||null;this.setRelated(null),this.onChange(this.instance,a,{_related:c})}}}),c.HasMany=c.Relation.extend({collectionType:null,options:{reverseRelation:{type:"HasOne"},collectionType:c.Collection,collectionKey:!0,collectionOptions:{}},initialize:function(){if(b.bindAll(this,"onChange","handleAddition","handleRemoval","handleReset"),this.instance.bind("relational:change:"+this.key,this.onChange),this.collectionType=this.options.collectionType,b.isString(this.collectionType)&&(this.collectionType=c.Relational.store.getObjectByName(this.collectionType)),!this.collectionType.prototype instanceof c.Collection)throw Error("collectionType must inherit from Backbone.Collection");this.keyContents instanceof c.Collection?this.setRelated(this._prepareCollection(this.keyContents)):this.setRelated(this._prepareCollection()),this.findRelated({silent:!0})},_getCollectionOptions:function(){return b.isFunction(this.options.collectionOptions)?this.options.collectionOptions(this.instance):this.options.collectionOptions},_prepareCollection:function(a){if(this.related&&this.related.unbind("relational:add",this.handleAddition).unbind("relational:remove",this.handleRemoval).unbind("relational:reset",this.handleReset),a&&a instanceof c.Collection||(a=new this.collectionType([],this._getCollectionOptions())),a.model=this.relatedModel,this.options.collectionKey){var b=this.options.collectionKey===!0?this.options.reverseRelation.key:this.options.collectionKey;a[b]&&a[b]!==this.instance?c.Relational.showWarnings&&"undefined"!=typeof console&&console.warn("Relation=%o; collectionKey=%s already exists on collection=%o",this,b,this.options.collectionKey):b&&(a[b]=this.instance)}return a.bind("relational:add",this.handleAddition).bind("relational:remove",this.handleRemoval).bind("relational:reset",this.handleReset),a},findRelated:function(a){if(this.keyContents){var d=[];this.keyContents instanceof c.Collection?d=this.keyContents.models:(this.keyContents=b.isArray(this.keyContents)?this.keyContents:[this.keyContents],b.each(this.keyContents||[],function(a){var b=null;a instanceof this.relatedModel?b=a:(a||0===a)&&(b=this.relatedModel.findOrCreate(a,{create:this.options.createModels})),b&&!this.related.get(b)&&d.push(b)},this)),d.length&&(a=this.unsanitizeOptions(a),this.related.add(d,a))}},onChange:function(d,e,f){if(f=this.sanitizeOptions(f),this.keyContents=e,e instanceof c.Collection)this._prepareCollection(e),this.related=e;else{var g={},h={};b.isArray(e)||e===a||(e=[e]),b.each(e,function(a){h[a.id]=!0});var i=this.related;i instanceof c.Collection?b.each(i.models.slice(0),function(a){f.keepNewModels&&a.isNew()||(g[a.id]=!0,i.remove(a,{silent:a.id in h}))}):i=this._prepareCollection(),b.each(e,function(a){var b=this.relatedModel.findOrCreate(a,{create:this.options.createModels});b&&i.add(b,{silent:a.id in g})},this),this.setRelated(i)}var j=this;c.Relational.eventQueue.add(function(){!f.silentChange&&j.instance.trigger("update:"+j.key,j.instance,j.related,f)})},tryAddRelated:function(a,d){if(d=this.sanitizeOptions(d),!this.related.get(a)){var e=b.any(this.keyContents||[],function(d){var e=c.Relational.store.resolveIdForItem(this.relatedModel,d);return!b.isNull(e)&&e===a.id},this);e&&this.related.add(a,d)}},handleAddition:function(a,d,e){if(a instanceof c.Model){e=this.sanitizeOptions(e),b.each(this.getReverseRelations(a)||[],function(a){a.addRelated(this.instance,e)},this);var f=this;c.Relational.eventQueue.add(function(){!e.silentChange&&f.instance.trigger("add:"+f.key,a,f.related,e)})}},handleRemoval:function(a,d,e){if(a instanceof c.Model){e=this.sanitizeOptions(e),b.each(this.getReverseRelations(a)||[],function(a){a.removeRelated(this.instance,e)},this);var f=this;c.Relational.eventQueue.add(function(){!e.silentChange&&f.instance.trigger("remove:"+f.key,a,f.related,e)})}},handleReset:function(a,b){b=this.sanitizeOptions(b);var d=this;c.Relational.eventQueue.add(function(){!b.silentChange&&d.instance.trigger("reset:"+d.key,d.related,b)})},addRelated:function(a,b){var c=this;b=this.unsanitizeOptions(b),a.queue(function(){c.related&&!c.related.get(a)&&c.related.add(a,b)})},removeRelated:function(a,b){b=this.unsanitizeOptions(b),this.related.get(a)&&this.related.remove(a,b)}}),c.RelationalModel=c.Model.extend({relations:null,_relations:null,_isInitialized:!1,_deferProcessing:!1,_queue:null,subModelTypeAttribute:"type",subModelTypes:null,constructor:function(a,d){var e=this;if(d&&d.collection){this._deferProcessing=!0;var f=function(a){a===e&&(e._deferProcessing=!1,e.processQueue(),d.collection.unbind("relational:add",f))};d.collection.bind("relational:add",f),b.defer(function(){f(e)})}this._queue=new c.BlockingQueue,this._queue.block(),c.Relational.eventQueue.block(),c.Model.apply(this,arguments),c.Relational.eventQueue.unblock()},trigger:function(a){if(a.length>5&&"change"===a.substr(0,6)){var b=this,d=arguments;c.Relational.eventQueue.add(function(){c.Model.prototype.trigger.apply(b,d)})}else c.Model.prototype.trigger.apply(this,arguments);return this},initializeRelations:function(){this.acquire(),this._relations=[],b.each(this.relations||[],function(a){var d=b.isString(a.type)?c[a.type]||c.Relational.store.getObjectByName(a.type):a.type;d&&d.prototype instanceof c.Relation?new d(this,a):c.Relational.showWarnings&&"undefined"!=typeof console&&console.warn("Relation=%o; missing or invalid type!",a)},this),this._isInitialized=!0,this.release(),this.processQueue()},updateRelations:function(a){this._isInitialized&&!this.isLocked()&&b.each(this._relations||[],function(b){var c=this.attributes[b.keySource]||this.attributes[b.key];b.related!==c&&this.trigger("relational:change:"+b.key,this,c,a||{})},this)},queue:function(a){this._queue.add(a)},processQueue:function(){this._isInitialized&&!this._deferProcessing&&this._queue.isBlocked()&&this._queue.unblock()},getRelation:function(c){return b.detect(this._relations,function(b){return b.key===c?!0:a},this)},getRelations:function(){return this._relations},fetchRelated:function(a,d,e){d||(d={});var f,g=[],h=this.getRelation(a),i=h&&h.keyContents,j=i&&b.select(b.isArray(i)?i:[i],function(a){var d=c.Relational.store.resolveIdForItem(h.relatedModel,a);return!b.isNull(d)&&(e||!c.Relational.store.find(h.relatedModel,d))},this);if(j&&j.length){var k=b.map(j,function(a){var c;if(b.isObject(a))c=h.relatedModel.findOrCreate(a);else{var d={};d[h.relatedModel.prototype.idAttribute]=a,c=h.relatedModel.findOrCreate(d)}return c},this);if(h.related instanceof c.Collection&&b.isFunction(h.related.url)&&(f=h.related.url(k)),f&&f!==h.related.url()){var l=b.defaults({error:function(){var a=arguments;b.each(k||[],function(b){b.trigger("destroy",b,b.collection,d),d.error&&d.error.apply(b,a)})},url:f},d,{add:!0});g=[h.related.fetch(l)]}else g=b.map(k||[],function(a){var c=b.defaults({error:function(){a.trigger("destroy",a,a.collection,d),d.error&&d.error.apply(a,arguments)}},d);return a.fetch(c)},this)}return g},set:function(a,d,e){c.Relational.eventQueue.block();var f;b.isObject(a)||null==a?(f=a,e=d):(f={},f[a]=d);var g=c.Model.prototype.set.apply(this,arguments);return this._isInitialized||this.isLocked()?f&&this.idAttribute in f&&c.Relational.store.update(this):(this.constructor.initializeModelHierarchy(),c.Relational.store.register(this),this.initializeRelations()),f&&this.updateRelations(e),c.Relational.eventQueue.unblock(),g},unset:function(a,b){c.Relational.eventQueue.block();var d=c.Model.prototype.unset.apply(this,arguments);return this.updateRelations(b),c.Relational.eventQueue.unblock(),d},clear:function(a){c.Relational.eventQueue.block();var b=c.Model.prototype.clear.apply(this,arguments);return this.updateRelations(a),c.Relational.eventQueue.unblock(),b},change:function(){var b=this,d=arguments;c.Relational.eventQueue.add(function(){c.Model.prototype.change.apply(b,d)})},clone:function(){var a=b.clone(this.attributes);return b.isUndefined(a[this.idAttribute])||(a[this.idAttribute]=null),b.each(this.getRelations()||[],function(b){delete a[b.key]}),new this.constructor(a)},toJSON:function(a){if(this.isLocked())return this.id;this.acquire();var d=c.Model.prototype.toJSON.call(this,a);return!this.constructor._superModel||this.constructor._subModelTypeAttribute in d||(d[this.constructor._subModelTypeAttribute]=this.constructor._subModelTypeValue),b.each(this._relations||[],function(e){var f=d[e.key];if(e.options.includeInJSON===!0)d[e.keyDestination]=f&&b.isFunction(f.toJSON)?f.toJSON(a):null;else if(b.isString(e.options.includeInJSON))d[e.keyDestination]=f instanceof c.Collection?f.pluck(e.options.includeInJSON):f instanceof c.Model?f.get(e.options.includeInJSON):null;else if(b.isArray(e.options.includeInJSON))if(f instanceof c.Collection){var g=[];f.each(function(a){var c={};b.each(e.options.includeInJSON,function(b){c[b]=a.get(b)}),g.push(c)}),d[e.keyDestination]=g}else if(f instanceof c.Model){var g={};b.each(e.options.includeInJSON,function(a){g[a]=f.get(a)}),d[e.keyDestination]=g}else d[e.keyDestination]=null;else delete d[e.key];e.keyDestination!==e.key&&delete d[e.key]}),this.release(),d}},{setup:function(){return this.prototype.relations=(this.prototype.relations||[]).slice(0),this._subModels={},this._superModel=null,this.prototype.hasOwnProperty("subModelTypes")?c.Relational.store.addSubModels(this.prototype.subModelTypes,this):this.prototype.subModelTypes=null,b.each(this.prototype.relations||[],function(a){if(a.model||(a.model=this),a.reverseRelation&&a.model===this){var d=!0;if(b.isString(a.relatedModel)){var e=c.Relational.store.getObjectByName(a.relatedModel);d=e&&e.prototype instanceof c.RelationalModel}var f=b.isString(a.type)?c[a.type]||c.Relational.store.getObjectByName(a.type):a.type;d&&f&&f.prototype instanceof c.Relation&&new f(null,a)}},this),this},build:function(a,b){var c=this;if(this.initializeModelHierarchy(),this._subModels&&this.prototype.subModelTypeAttribute in a){var d=a[this.prototype.subModelTypeAttribute],e=this._subModels[d];e&&(c=e)}return new c(a,b)},initializeModelHierarchy:function(){if(b.isUndefined(this._superModel)||b.isNull(this._superModel))if(c.Relational.store.setupSuperModel(this),this._superModel){if(this._superModel.prototype.relations){var a=b.any(this.prototype.relations||[],function(a){return a.model&&a.model!==this},this);a||(this.prototype.relations=this._superModel.prototype.relations.concat(this.prototype.relations))}}else this._superModel=!1;this.prototype.subModelTypes&&b.keys(this.prototype.subModelTypes).length!==b.keys(this._subModels).length&&b.each(this.prototype.subModelTypes||[],function(a){var b=c.Relational.store.getObjectByName(a);b&&b.initializeModelHierarchy()})},findOrCreate:function(a,d){var e=b.isObject(a)&&this.prototype.parse?this.prototype.parse(a):a,f=c.Relational.store.find(this,e);return b.isObject(a)&&(f?f.set(e,d):(!d||d&&d.create!==!1)&&(f=this.build(a,d))),f}}),b.extend(c.RelationalModel.prototype,c.Semaphore),c.Collection.prototype.__prepareModel=c.Collection.prototype._prepareModel,c.Collection.prototype._prepareModel=function(b,d){var e;return b instanceof c.Model?(b.collection||(b.collection=this),e=b):(d||(d={}),d.collection=this,e=this.model.findOrCreate!==a?this.model.findOrCreate(b,d):new this.model(b,d),e._validate(b,d)||(e=!1)),e};var e=c.Collection.prototype.__add=c.Collection.prototype.add;c.Collection.prototype.add=function(a,d){d||(d={}),b.isArray(a)||(a=[a]);var f=[];return b.each(a||[],function(a){a instanceof c.Model||(a=c.Collection.prototype._prepareModel.call(this,a,d)),a instanceof c.Model&&!this.get(a)&&f.push(a)},this),f.length&&(e.call(this,f,d),b.each(f||[],function(a){this.trigger("relational:add",a,this,d)},this)),this};var f=c.Collection.prototype.__remove=c.Collection.prototype.remove;c.Collection.prototype.remove=function(a,d){return d||(d={}),a=b.isArray(a)?a.slice(0):[a],b.each(a||[],function(a){a=this.get(a),a instanceof c.Model&&(f.call(this,a,d),this.trigger("relational:remove",a,this,d))},this),this};var g=c.Collection.prototype.__reset=c.Collection.prototype.reset;c.Collection.prototype.reset=function(a,b){return g.call(this,a,b),this.trigger("relational:reset",this,b),this};var h=c.Collection.prototype.__sort=c.Collection.prototype.sort;c.Collection.prototype.sort=function(a){return h.call(this,a),this.trigger("relational:reset",this,a),this};var i=c.Collection.prototype.__trigger=c.Collection.prototype.trigger;c.Collection.prototype.trigger=function(a){if("add"===a||"remove"===a||"reset"===a){var d=this,e=arguments;"add"===a&&(e=b.toArray(e),b.isObject(e[3])&&(e[3]=b.clone(e[3]))),c.Relational.eventQueue.add(function(){i.apply(d,e)})}else i.apply(this,arguments);return this},c.RelationalModel.extend=function(){var d=c.Model.extend.apply(this,arguments);return d.setup(this),d}})(); | ||
(function(undefined){"use strict";var _,Backbone,exports;if(typeof window==="undefined"){_=require("underscore");Backbone=require("backbone");exports=module.exports=Backbone}else{_=window._;Backbone=window.Backbone;exports=window}Backbone.Relational={showWarnings:true};Backbone.Semaphore={_permitsAvailable:null,_permitsUsed:0,acquire:function(){if(this._permitsAvailable&&this._permitsUsed>=this._permitsAvailable){throw new Error("Max permits acquired")}else{this._permitsUsed++}},release:function(){if(this._permitsUsed===0){throw new Error("All permits released")}else{this._permitsUsed--}},isLocked:function(){return this._permitsUsed>0},setAvailablePermits:function(amount){if(this._permitsUsed>amount){throw new Error("Available permits cannot be less than used permits")}this._permitsAvailable=amount}};Backbone.BlockingQueue=function(){this._queue=[]};_.extend(Backbone.BlockingQueue.prototype,Backbone.Semaphore,{_queue:null,add:function(func){if(this.isBlocked()){this._queue.push(func)}else{func()}},process:function(){while(this._queue&&this._queue.length){this._queue.shift()()}},block:function(){this.acquire()},unblock:function(){this.release();if(!this.isBlocked()){this.process()}},isBlocked:function(){return this.isLocked()}});Backbone.Relational.eventQueue=new Backbone.BlockingQueue;Backbone.Store=function(){this._collections=[];this._reverseRelations=[];this._orphanRelations=[];this._subModels=[];this._modelScopes=[exports]};_.extend(Backbone.Store.prototype,Backbone.Events,{initializeRelation:function(model,relation,options){var type=!_.isString(relation.type)?relation.type:Backbone[relation.type]||this.getObjectByName(relation.type);if(type&&type.prototype instanceof Backbone.Relation){new type(model,relation,options)}else{Backbone.Relational.showWarnings&&typeof console!=="undefined"&&console.warn("Relation=%o; missing or invalid relation type!",relation)}},addModelScope:function(scope){this._modelScopes.push(scope)},addSubModels:function(subModelTypes,superModelType){this._subModels.push({superModelType:superModelType,subModels:subModelTypes})},setupSuperModel:function(modelType){_.find(this._subModels,function(subModelDef){return _.find(subModelDef.subModels||[],function(subModelTypeName,typeValue){var subModelType=this.getObjectByName(subModelTypeName);if(modelType===subModelType){subModelDef.superModelType._subModels[typeValue]=modelType;modelType._superModel=subModelDef.superModelType;modelType._subModelTypeValue=typeValue;modelType._subModelTypeAttribute=subModelDef.superModelType.prototype.subModelTypeAttribute;return true}},this)},this)},addReverseRelation:function(relation){var exists=_.any(this._reverseRelations,function(rel){return _.all(relation||[],function(val,key){return val===rel[key]})});if(!exists&&relation.model&&relation.type){this._reverseRelations.push(relation);this._addRelation(relation.model,relation);this.retroFitRelation(relation)}},addOrphanRelation:function(relation){var exists=_.any(this._orphanRelations,function(rel){return _.all(relation||[],function(val,key){return val===rel[key]})});if(!exists&&relation.model&&relation.type){this._orphanRelations.push(relation)}},processOrphanRelations:function(){_.each(this._orphanRelations.slice(0),function(rel){var relatedModel=Backbone.Relational.store.getObjectByName(rel.relatedModel);if(relatedModel){this.initializeRelation(null,rel);this._orphanRelations=_.without(this._orphanRelations,rel)}},this)},_addRelation:function(type,relation){if(!type.prototype.relations){type.prototype.relations=[]}type.prototype.relations.push(relation);_.each(type._subModels||[],function(subModel){this._addRelation(subModel,relation)},this)},retroFitRelation:function(relation){var coll=this.getCollection(relation.model,false);coll&&coll.each(function(model){if(!(model instanceof relation.model)){return}new relation.type(model,relation)},this)},getCollection:function(type,create){if(type instanceof Backbone.RelationalModel){type=type.constructor}var rootModel=type;while(rootModel._superModel){rootModel=rootModel._superModel}var coll=_.findWhere(this._collections,{model:rootModel});if(!coll&&create!==false){coll=this._createCollection(rootModel)}return coll},getObjectByName:function(name){var parts=name.split("."),type=null;_.find(this._modelScopes,function(scope){type=_.reduce(parts||[],function(memo,val){return memo?memo[val]:undefined},scope);if(type&&type!==scope){return true}},this);return type},_createCollection:function(type){var coll;if(type instanceof Backbone.RelationalModel){type=type.constructor}if(type.prototype instanceof Backbone.RelationalModel){coll=new Backbone.Collection;coll.model=type;this._collections.push(coll)}return coll},resolveIdForItem:function(type,item){var id=_.isString(item)||_.isNumber(item)?item:null;if(id===null){if(item instanceof Backbone.RelationalModel){id=item.id}else if(_.isObject(item)){id=item[type.prototype.idAttribute]}}if(!id&&id!==0){id=null}return id},find:function(type,item){var id=this.resolveIdForItem(type,item);var coll=this.getCollection(type);if(coll){var obj=coll.get(id);if(obj instanceof type){return obj}}return null},register:function(model){var coll=this.getCollection(model);if(coll){if(coll.get(model)){if(Backbone.Relational.showWarnings&&typeof console!=="undefined"){console.warn("Duplicate id! Old RelationalModel=%o, new RelationalModel=%o",coll.get(model),model)}throw new Error("Cannot instantiate more than one Backbone.RelationalModel with the same id per type!")}var modelColl=model.collection;coll.add(model);this.listenTo(model,"destroy",this.unregister,this);model.collection=modelColl}},update:function(model){var coll=this.getCollection(model);coll._onModelEvent("change:"+model.idAttribute,model,coll)},unregister:function(model){this.stopListening(model,"destroy",this.unregister);var coll=this.getCollection(model);coll&&coll.remove(model)},reset:function(){this.stopListening();this._collections=[];this._subModels=[];this._modelScopes=[exports]}});Backbone.Relational.store=new Backbone.Store;Backbone.Relation=function(instance,options,opts){this.instance=instance;options=_.isObject(options)?options:{};this.reverseRelation=_.defaults(options.reverseRelation||{},this.options.reverseRelation);this.options=_.defaults(options,this.options,Backbone.Relation.prototype.options);this.reverseRelation.type=!_.isString(this.reverseRelation.type)?this.reverseRelation.type:Backbone[this.reverseRelation.type]||Backbone.Relational.store.getObjectByName(this.reverseRelation.type);this.key=this.options.key;this.keySource=this.options.keySource||this.key;this.keyDestination=this.options.keyDestination||this.keySource||this.key;this.model=this.options.model||this.instance.constructor;this.relatedModel=this.options.relatedModel;if(_.isString(this.relatedModel)){this.relatedModel=Backbone.Relational.store.getObjectByName(this.relatedModel)}if(!this.checkPreconditions()){return}if(!this.options.isAutoRelation&&this.reverseRelation.type&&this.reverseRelation.key){Backbone.Relational.store.addReverseRelation(_.defaults({isAutoRelation:true,model:this.relatedModel,relatedModel:this.model,reverseRelation:this.options},this.reverseRelation))}if(instance){var contentKey=this.keySource;if(contentKey!==this.key&&typeof this.instance.get(this.key)==="object"){contentKey=this.key}this.setKeyContents(this.instance.get(contentKey));this.relatedCollection=Backbone.Relational.store.getCollection(this.relatedModel);if(this.keySource!==this.key){this.instance.unset(this.keySource,{silent:true})}this.instance._relations[this.key]=this;this.initialize(opts);if(this.options.autoFetch){this.instance.fetchRelated(this.key,_.isObject(this.options.autoFetch)?this.options.autoFetch:{})}this.listenTo(this.instance,"destroy",this.destroy).listenTo(this.relatedCollection,"relational:add",this.tryAddRelated).listenTo(this.relatedCollection,"relational:remove",this.removeRelated)}};Backbone.Relation.extend=Backbone.Model.extend;_.extend(Backbone.Relation.prototype,Backbone.Events,Backbone.Semaphore,{options:{createModels:true,includeInJSON:true,isAutoRelation:false,autoFetch:false,parse:false},instance:null,key:null,keyContents:null,relatedModel:null,relatedCollection:null,reverseRelation:null,related:null,checkPreconditions:function(){var i=this.instance,k=this.key,m=this.model,rm=this.relatedModel,warn=Backbone.Relational.showWarnings&&typeof console!=="undefined";if(!m||!k||!rm){warn&&console.warn("Relation=%o: missing model, key or relatedModel (%o, %o, %o).",this,m,k,rm);return false}if(!(m.prototype instanceof Backbone.RelationalModel)){warn&&console.warn("Relation=%o: model does not inherit from Backbone.RelationalModel (%o).",this,i);return false}if(!(rm.prototype instanceof Backbone.RelationalModel)){warn&&console.warn("Relation=%o: relatedModel does not inherit from Backbone.RelationalModel (%o).",this,rm);return false}if(this instanceof Backbone.HasMany&&this.reverseRelation.type===Backbone.HasMany){warn&&console.warn("Relation=%o: relation is a HasMany, and the reverseRelation is HasMany as well.",this);return false}if(i&&_.keys(i._relations).length){var existing=_.find(i._relations,function(rel){return rel.key===k},this);if(existing){warn&&console.warn("Cannot create relation=%o on %o for model=%o: already taken by relation=%o.",this,k,i,existing);return false}}return true},setRelated:function(related){this.related=related;this.instance.acquire();this.instance.attributes[this.key]=related;this.instance.release()},_isReverseRelation:function(relation){return relation.instance instanceof this.relatedModel&&this.reverseRelation.key===relation.key&&this.key===relation.reverseRelation.key},getReverseRelations:function(model){var reverseRelations=[];var models=!_.isUndefined(model)?[model]:this.related&&(this.related.models||[this.related]);_.each(models||[],function(related){_.each(related.getRelations()||[],function(relation){if(this._isReverseRelation(relation)){reverseRelations.push(relation)}},this)},this);return reverseRelations},destroy:function(){this.stopListening();if(this instanceof Backbone.HasOne){this.setRelated(null)}else if(this instanceof Backbone.HasMany){this.setRelated(this._prepareCollection())}_.each(this.getReverseRelations(),function(relation){relation.removeRelated(this.instance)},this)}});Backbone.HasOne=Backbone.Relation.extend({options:{reverseRelation:{type:"HasMany"}},initialize:function(opts){this.listenTo(this.instance,"relational:change:"+this.key,this.onChange);var related=this.findRelated(opts);this.setRelated(related);_.each(this.getReverseRelations(),function(relation){relation.addRelated(this.instance,opts)},this)},findRelated:function(options){var related=null;options=_.defaults({parse:this.options.parse},options);if(this.keyContents instanceof this.relatedModel){related=this.keyContents}else if(this.keyContents||this.keyContents===0){var opts=_.defaults({create:this.options.createModels},options);related=this.relatedModel.findOrCreate(this.keyContents,opts)}return related},setKeyContents:function(keyContents){this.keyContents=keyContents;this.keyId=Backbone.Relational.store.resolveIdForItem(this.relatedModel,this.keyContents)},onChange:function(model,attr,options){if(this.isLocked()){return}this.acquire();options=options?_.clone(options):{};var changed=_.isUndefined(options.__related),oldRelated=changed?this.related:options.__related;if(changed){this.setKeyContents(attr);var related=this.findRelated(options);this.setRelated(related)}if(oldRelated&&this.related!==oldRelated){_.each(this.getReverseRelations(oldRelated),function(relation){relation.removeRelated(this.instance,null,options)},this)}_.each(this.getReverseRelations(),function(relation){relation.addRelated(this.instance,options)},this);if(!options.silent&&this.related!==oldRelated){var dit=this;this.changed=true;Backbone.Relational.eventQueue.add(function(){dit.instance.trigger("change:"+dit.key,dit.instance,dit.related,options,true);dit.changed=false})}this.release()},tryAddRelated:function(model,coll,options){if((this.keyId||this.keyId===0)&&model.id===this.keyId){this.addRelated(model,options);this.keyId=null}},addRelated:function(model,options){var dit=this;model.queue(function(){if(model!==dit.related){var oldRelated=dit.related||null;dit.setRelated(model);dit.onChange(dit.instance,model,_.defaults({__related:oldRelated},options))}})},removeRelated:function(model,coll,options){if(!this.related){return}if(model===this.related){var oldRelated=this.related||null;this.setRelated(null);this.onChange(this.instance,model,_.defaults({__related:oldRelated},options))}}});Backbone.HasMany=Backbone.Relation.extend({collectionType:null,options:{reverseRelation:{type:"HasOne"},collectionType:Backbone.Collection,collectionKey:true,collectionOptions:{}},initialize:function(opts){this.listenTo(this.instance,"relational:change:"+this.key,this.onChange);this.collectionType=this.options.collectionType;if(_.isString(this.collectionType)){this.collectionType=Backbone.Relational.store.getObjectByName(this.collectionType)}if(!this.collectionType.prototype instanceof Backbone.Collection){throw new Error("`collectionType` must inherit from Backbone.Collection")}var related=this.findRelated(opts);this.setRelated(related)},_prepareCollection:function(collection){if(this.related){this.stopListening(this.related)}if(!collection||!(collection instanceof Backbone.Collection)){var options=_.isFunction(this.options.collectionOptions)?this.options.collectionOptions(this.instance):this.options.collectionOptions;collection=new this.collectionType(null,options)}collection.model=this.relatedModel;if(this.options.collectionKey){var key=this.options.collectionKey===true?this.options.reverseRelation.key:this.options.collectionKey;if(collection[key]&&collection[key]!==this.instance){if(Backbone.Relational.showWarnings&&typeof console!=="undefined"){console.warn("Relation=%o; collectionKey=%s already exists on collection=%o",this,key,this.options.collectionKey)}}else if(key){collection[key]=this.instance}}this.listenTo(collection,"relational:add",this.handleAddition).listenTo(collection,"relational:remove",this.handleRemoval).listenTo(collection,"relational:reset",this.handleReset);return collection},findRelated:function(options){var related=null;options=_.defaults({parse:this.options.parse},options);if(this.keyContents instanceof Backbone.Collection){this._prepareCollection(this.keyContents);related=this.keyContents}else{var toAdd=[];_.each(this.keyContents,function(attributes){if(attributes instanceof this.relatedModel){var model=attributes}else{model=this.relatedModel.findOrCreate(attributes,_.extend({merge:true},options,{create:this.options.createModels}))}model&&toAdd.push(model)},this);if(this.related instanceof Backbone.Collection){related=this.related}else{related=this._prepareCollection()}related.update(toAdd,_.defaults({merge:false,parse:false},options))}return related},setKeyContents:function(keyContents){this.keyContents=keyContents instanceof Backbone.Collection?keyContents:null;this.keyIds=[];if(!this.keyContents&&(keyContents||keyContents===0)){this.keyContents=_.isArray(keyContents)?keyContents:[keyContents];_.each(this.keyContents,function(item){var itemId=Backbone.Relational.store.resolveIdForItem(this.relatedModel,item);if(itemId||itemId===0){this.keyIds.push(itemId)}},this)}},onChange:function(model,attr,options){options=options?_.clone(options):{};this.setKeyContents(attr);this.changed=false;var related=this.findRelated(options);this.setRelated(related);if(!options.silent){var dit=this;Backbone.Relational.eventQueue.add(function(){if(dit.changed){dit.instance.trigger("change:"+dit.key,dit.instance,dit.related,options,true);dit.changed=false}})}},handleAddition:function(model,coll,options){options=options?_.clone(options):{};this.changed=true;_.each(this.getReverseRelations(model),function(relation){relation.addRelated(this.instance,options)},this);var dit=this;!options.silent&&Backbone.Relational.eventQueue.add(function(){dit.instance.trigger("add:"+dit.key,model,dit.related,options)})},handleRemoval:function(model,coll,options){options=options?_.clone(options):{};this.changed=true;_.each(this.getReverseRelations(model),function(relation){relation.removeRelated(this.instance,null,options)},this);var dit=this;!options.silent&&Backbone.Relational.eventQueue.add(function(){dit.instance.trigger("remove:"+dit.key,model,dit.related,options)})},handleReset:function(coll,options){var dit=this;options=options?_.clone(options):{};!options.silent&&Backbone.Relational.eventQueue.add(function(){dit.instance.trigger("reset:"+dit.key,dit.related,options)})},tryAddRelated:function(model,coll,options){var item=_.contains(this.keyIds,model.id);if(item){this.addRelated(model,options);this.keyIds=_.without(this.keyIds,model.id)}},addRelated:function(model,options){var dit=this;model.queue(function(){if(dit.related&&!dit.related.get(model)){dit.related.add(model,options)}})},removeRelated:function(model,coll,options){if(this.related.get(model)){this.related.remove(model,options)}}});Backbone.RelationalModel=Backbone.Model.extend({relations:null,_relations:null,_isInitialized:false,_deferProcessing:false,_queue:null,subModelTypeAttribute:"type",subModelTypes:null,constructor:function(attributes,options){if(options&&options.collection){var dit=this,collection=this.collection=options.collection;delete options.collection;this._deferProcessing=true;var processQueue=function(model){if(model===dit){dit._deferProcessing=false;dit.processQueue();collection.off("relational:add",processQueue)}};collection.on("relational:add",processQueue);_.defer(function(){processQueue(dit)})}Backbone.Relational.store.processOrphanRelations();this._queue=new Backbone.BlockingQueue;this._queue.block();Backbone.Relational.eventQueue.block();try{Backbone.Model.apply(this,arguments)}finally{Backbone.Relational.eventQueue.unblock()}},trigger:function(eventName){if(eventName.length>5&&eventName.indexOf("change")===0){var dit=this,args=arguments;Backbone.Relational.eventQueue.add(function(){if(!dit._isInitialized){return}var changed=true;if(eventName==="change"){changed=dit.hasChanged()}else{var attr=eventName.slice(7),rel=dit.getRelation(attr);if(rel){changed=args[4]===true;if(changed){dit.changed[attr]=args[2]}else if(!rel.changed){delete dit.changed[attr]}}}changed&&Backbone.Model.prototype.trigger.apply(dit,args)})}else{Backbone.Model.prototype.trigger.apply(this,arguments)}return this},initializeRelations:function(options){this.acquire();this._relations={};_.each(this.relations||[],function(rel){Backbone.Relational.store.initializeRelation(this,rel,options)},this);this._isInitialized=true;this.release();this.processQueue()},updateRelations:function(options){if(this._isInitialized&&!this.isLocked()){_.each(this._relations,function(rel){var val=this.attributes[rel.keySource]||this.attributes[rel.key];if(rel.related!==val){this.trigger("relational:change:"+rel.key,this,val,options||{})}},this)}},queue:function(func){this._queue.add(func)},processQueue:function(){if(this._isInitialized&&!this._deferProcessing&&this._queue.isBlocked()){this._queue.unblock()}},getRelation:function(key){return this._relations[key]},getRelations:function(){return _.values(this._relations)},fetchRelated:function(key,options,refresh){options=_.extend({update:true,remove:false},options);var setUrl,requests=[],rel=this.getRelation(key),keys=rel&&(rel.keyIds||[rel.keyId]),toFetch=keys&&_.select(keys||[],function(id){return(id||id===0)&&(refresh||!Backbone.Relational.store.find(rel.relatedModel,id))},this);if(toFetch&&toFetch.length){var created=[],models=_.map(toFetch,function(id){var model=Backbone.Relational.store.find(rel.relatedModel,id);if(!model){var attrs={};attrs[rel.relatedModel.prototype.idAttribute]=id;model=rel.relatedModel.findOrCreate(attrs,options);created.push(model)}return model},this);if(rel.related instanceof Backbone.Collection&&_.isFunction(rel.related.url)){setUrl=rel.related.url(models)}if(setUrl&&setUrl!==rel.related.url()){var opts=_.defaults({error:function(){var args=arguments;_.each(created,function(model){model.trigger("destroy",model,model.collection,options);options.error&&options.error.apply(model,args)})},url:setUrl},options);requests=[rel.related.fetch(opts)]}else{requests=_.map(models,function(model){var opts=_.defaults({error:function(){if(_.contains(created,model)){model.trigger("destroy",model,model.collection,options);options.error&&options.error.apply(model,arguments)}}},options);return model.fetch(opts)},this)}}return requests},get:function(attr){var originalResult=Backbone.Model.prototype.get.call(this,attr);if(!this.dotNotation||attr.indexOf(".")===-1){return originalResult}var splits=attr.split(".");var result=_.reduce(splits,function(model,split){if(!(model instanceof Backbone.Model)){throw new Error("Attribute must be an instanceof Backbone.Model. Is: "+model+", currentSplit: "+split)}return Backbone.Model.prototype.get.call(model,split)},this);if(originalResult!==undefined&&result!==undefined){throw new Error("Ambiguous result for '"+attr+"'. direct result: "+originalResult+", dotNotation: "+result)}return originalResult||result},set:function(key,value,options){Backbone.Relational.eventQueue.block();var attributes;if(_.isObject(key)||key==null){attributes=key;options=value}else{attributes={};attributes[key]=value}var result=Backbone.Model.prototype.set.apply(this,arguments);try{if(!this._isInitialized&&!this.isLocked()){this.constructor.initializeModelHierarchy();Backbone.Relational.store.register(this);this.initializeRelations(options)}else if(attributes&&this.idAttribute in attributes){Backbone.Relational.store.update(this)}if(attributes){this.updateRelations(options)}}finally{Backbone.Relational.eventQueue.unblock()}return result},unset:function(attribute,options){Backbone.Relational.eventQueue.block();var result=Backbone.Model.prototype.unset.apply(this,arguments);this.updateRelations(options);Backbone.Relational.eventQueue.unblock();return result},clear:function(options){Backbone.Relational.eventQueue.block();var result=Backbone.Model.prototype.clear.apply(this,arguments);this.updateRelations(options);Backbone.Relational.eventQueue.unblock();return result},clone:function(){var attributes=_.clone(this.attributes);if(!_.isUndefined(attributes[this.idAttribute])){attributes[this.idAttribute]=null}_.each(this.getRelations(),function(rel){delete attributes[rel.key]});return new this.constructor(attributes)},toJSON:function(options){if(this.isLocked()){return this.id}this.acquire();var json=Backbone.Model.prototype.toJSON.call(this,options);if(this.constructor._superModel&&!(this.constructor._subModelTypeAttribute in json)){json[this.constructor._subModelTypeAttribute]=this.constructor._subModelTypeValue}_.each(this._relations,function(rel){var value=json[rel.key];if(rel.options.includeInJSON===true){if(value&&_.isFunction(value.toJSON)){json[rel.keyDestination]=value.toJSON(options)}else{json[rel.keyDestination]=null}}else if(_.isString(rel.options.includeInJSON)){if(value instanceof Backbone.Collection){json[rel.keyDestination]=value.pluck(rel.options.includeInJSON)}else if(value instanceof Backbone.Model){json[rel.keyDestination]=value.get(rel.options.includeInJSON)}else{json[rel.keyDestination]=null}}else if(_.isArray(rel.options.includeInJSON)){if(value instanceof Backbone.Collection){var valueSub=[];value.each(function(model){var curJson={};_.each(rel.options.includeInJSON,function(key){curJson[key]=model.get(key)});valueSub.push(curJson)});json[rel.keyDestination]=valueSub}else if(value instanceof Backbone.Model){var valueSub={};_.each(rel.options.includeInJSON,function(key){valueSub[key]=value.get(key)});json[rel.keyDestination]=valueSub}else{json[rel.keyDestination]=null}}else{delete json[rel.key]}if(rel.keyDestination!==rel.key){delete json[rel.key]}});this.release();return json}},{setup:function(superModel){this.prototype.relations=(this.prototype.relations||[]).slice(0);this._subModels={};this._superModel=null;if(this.prototype.hasOwnProperty("subModelTypes")){Backbone.Relational.store.addSubModels(this.prototype.subModelTypes,this)}else{this.prototype.subModelTypes=null}_.each(this.prototype.relations||[],function(rel){if(!rel.model){rel.model=this}if(rel.reverseRelation&&rel.model===this){var preInitialize=true;if(_.isString(rel.relatedModel)){var relatedModel=Backbone.Relational.store.getObjectByName(rel.relatedModel);preInitialize=relatedModel&&relatedModel.prototype instanceof Backbone.RelationalModel}if(preInitialize){Backbone.Relational.store.initializeRelation(null,rel)}else if(_.isString(rel.relatedModel)){Backbone.Relational.store.addOrphanRelation(rel)}}},this);return this},build:function(attributes,options){var model=this;this.initializeModelHierarchy();if(this._subModels&&this.prototype.subModelTypeAttribute in attributes){var subModelTypeAttribute=attributes[this.prototype.subModelTypeAttribute];var subModelType=this._subModels[subModelTypeAttribute];if(subModelType){model=subModelType}}return new model(attributes,options)},initializeModelHierarchy:function(){if(_.isUndefined(this._superModel)||_.isNull(this._superModel)){Backbone.Relational.store.setupSuperModel(this);if(this._superModel){if(this._superModel.prototype.relations){var supermodelRelationsExist=_.any(this.prototype.relations||[],function(rel){return rel.model&&rel.model!==this},this);if(!supermodelRelationsExist){this.prototype.relations=this._superModel.prototype.relations.concat(this.prototype.relations)}}}else{this._superModel=false}}if(this.prototype.subModelTypes&&_.keys(this.prototype.subModelTypes).length!==_.keys(this._subModels).length){_.each(this.prototype.subModelTypes||[],function(subModelTypeName){var subModelType=Backbone.Relational.store.getObjectByName(subModelTypeName);subModelType&&subModelType.initializeModelHierarchy()})}},findOrCreate:function(attributes,options){options||(options={});var parsedAttributes=_.isObject(attributes)&&options.parse&&this.prototype.parse?this.prototype.parse(attributes):attributes;var model=Backbone.Relational.store.find(this,parsedAttributes);if(_.isObject(attributes)){if(model&&options.merge!==false){model.set(parsedAttributes,options)}else if(!model&&options.create!==false){model=this.build(attributes,options)}}return model}});_.extend(Backbone.RelationalModel.prototype,Backbone.Semaphore);Backbone.Collection.prototype.__prepareModel=Backbone.Collection.prototype._prepareModel;Backbone.Collection.prototype._prepareModel=function(attrs,options){var model;if(attrs instanceof Backbone.Model){if(!attrs.collection){attrs.collection=this}model=attrs}else{options||(options={});options.collection=this;if(typeof this.model.findOrCreate!=="undefined"){model=this.model.findOrCreate(attrs,options)}else{model=new this.model(attrs,options)}if(model&&model.isNew()&&!model._validate(attrs,options)){this.trigger("invalid",this,attrs,options);model=false}}return model};var add=Backbone.Collection.prototype.__add=Backbone.Collection.prototype.add;Backbone.Collection.prototype.add=function(models,options){if(!(this.model.prototype instanceof Backbone.RelationalModel)){return add.apply(this,arguments)}models=_.isArray(models)?models.slice():[models];options=_.extend({merge:false},options);var newModels=[],toAdd=[];_.each(models,function(model){if(!(model instanceof Backbone.Model)){model=Backbone.Collection.prototype._prepareModel.call(this,model,options)}if(model){toAdd.push(model);if(!(this.get(model)||this.get(model.cid))){newModels.push(model)}else if(model.id!=null){this._byId[model.id]=model}}},this);add.call(this,toAdd,options);_.each(newModels,function(model){if(this.get(model)||this.get(model.cid)){this.trigger("relational:add",model,this,options)}},this);return this};var remove=Backbone.Collection.prototype.__remove=Backbone.Collection.prototype.remove;Backbone.Collection.prototype.remove=function(models,options){if(!(this.model.prototype instanceof Backbone.RelationalModel)){return remove.apply(this,arguments)}models=_.isArray(models)?models.slice():[models];options||(options={});var toRemove=[];_.each(models,function(model){model=this.get(model)||this.get(model.cid);model&&toRemove.push(model)},this);if(toRemove.length){remove.call(this,toRemove,options);_.each(toRemove,function(model){this.trigger("relational:remove",model,this,options)},this)}return this};var reset=Backbone.Collection.prototype.__reset=Backbone.Collection.prototype.reset;Backbone.Collection.prototype.reset=function(models,options){reset.call(this,models,options);if(this.model.prototype instanceof Backbone.RelationalModel){this.trigger("relational:reset",this,options)}return this};var sort=Backbone.Collection.prototype.__sort=Backbone.Collection.prototype.sort;Backbone.Collection.prototype.sort=function(options){sort.call(this,options);if(this.model.prototype instanceof Backbone.RelationalModel){this.trigger("relational:reset",this,options)}return this};var trigger=Backbone.Collection.prototype.__trigger=Backbone.Collection.prototype.trigger;Backbone.Collection.prototype.trigger=function(eventName){if(!(this.model.prototype instanceof Backbone.RelationalModel)){return trigger.apply(this,arguments)}if(eventName==="add"||eventName==="remove"||eventName==="reset"){var dit=this,args=arguments;if(_.isObject(args[3])){args=_.toArray(args);args[3]=_.clone(args[3])}Backbone.Relational.eventQueue.add(function(){trigger.apply(dit,args)})}else{trigger.apply(this,arguments)}return this};Backbone.RelationalModel.extend=function(protoProps,classProps){var child=Backbone.Model.extend.apply(this,arguments);child.setup(this);return child}})(); |
@@ -16,4 +16,4 @@ { | ||
"optionalDependencies": { | ||
"underscore": ">=1.4.3", | ||
"backbone": ">=0.9.9" | ||
"underscore": ">=1.4.4", | ||
"backbone": ">=0.9.10" | ||
}, | ||
@@ -23,3 +23,3 @@ | ||
"main" : "backbone-relational.js", | ||
"version" : "0.7.0" | ||
"version" : "0.8.0" | ||
} |
90472
1551