diff --git a/CHANGELOG.md b/CHANGELOG.md index d838493..b953a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ -### Upcoming (Edge version) +### Version 0.5.1 - [Diff](https://github.com/dhruvaray/backbone-associations/compare/v0.5.0...v0.5.1) * `relatedModel` can now also be a function. Could be used to model polymorphic associations like in this [gist](https://gist.github.com/dhruvaray/5988996). * `relatedModel` can be passed a special value called `Backbone.self` to handle self references. -* Introducing explict cleanup on `Backbone.Model` to prevent memory leaks when users set `Backbone.Model` to undefined explictly. +* Introducing explicit cleanup on `Backbone.Model` to prevent memory leaks when users set `Backbone.Model` to undefined explicitly. * Fixed the handling of {reset:true} option on `Backbone.Collection`. * Fixed a scenario where repeated fetch on the same `Backbone.Model` or `Backbone.Collection` caused event bubbling to stop. diff --git a/backbone-associations-min.js b/backbone-associations-min.js index 77d398d..86f9596 100644 --- a/backbone-associations-min.js +++ b/backbone-associations-min.js @@ -1,11 +1,12 @@ -(function(){var v=this,g,h,w,m,r,s,z,o,A,B;"undefined"===typeof window?(g=require("underscore"),h=require("backbone"),"undefined"!==typeof exports&&(exports=module.exports=h)):(g=v._,h=v.Backbone);w=h.Model;m=h.Collection;r=w.prototype;s=m.prototype;A=/[\.\[\]]+/g;z="change add remove reset sort destroy".split(" ");B=["reset","sort"];h.Associations={VERSION:"0.5.0"};h.Associations.Many=h.Many="Many";h.Associations.One=h.One="One";o=h.AssociatedModel=h.Associations.AssociatedModel=w.extend({relations:void 0, -_proxyCalls:void 0,get:function(a){var c=r.get.call(this,a);return c?c:this._getAttr.apply(this,arguments)},set:function(a,c,d){var b;if(g.isObject(a)||a==null){b=a;d=c}else{b={};b[a]=c}a=this._set(b,d);this._processPendingEvents();return a},_set:function(a,c){var d,b,n,f,j=this;if(!a)return this;for(d in a){b||(b={});if(d.match(A)){var k=x(d);f=g.initial(k);k=k[k.length-1];f=this.get(f);if(f instanceof o){f=b[f.cid]||(b[f.cid]={model:f,data:{}});f.data[k]=a[d]}}else{f=b[this.cid]||(b[this.cid]={model:this, -data:{}});f.data[d]=a[d]}}if(b)for(n in b){f=b[n];this._setAttr.call(f.model,f.data,c)||(j=false)}else j=this._setAttr.call(this,a,c);return j},_setAttr:function(a,c){var d;c||(c={});if(c.unset)for(d in a)a[d]=void 0;this.parents=this.parents||[];this.relations&&g.each(this.relations,function(b){var d=b.key,f=b.relatedModel,j=b.collectionType,k=b.map,i=this.attributes[d],y=i&&i.idAttribute,e,q,l,p;f&&g.isString(f)&&(f=t(f));j&&g.isString(j)&&(j=t(j));k&&g.isString(k)&&(k=t(k));q=b.options?g.extend({}, -b.options,c):c;if(a[d]){e=g.result(a,d);e=k?k(e):e;if(b.type===h.Many){if(j&&!j.prototype instanceof m)throw Error("collectionType must inherit from Backbone.Collection");if(e instanceof m)l=e;else if(i){i._deferEvents=true;i.set(e,c);l=i}else{l=j?new j:this._createCollection(f);l.add(e,q)}}else if(b.type===h.One&&f)if(e instanceof o)l=e;else if(i)if(i&&e[y]&&i.get(y)===e[y]){i._deferEvents=true;i._set(e,c);l=i}else l=new f(e,q);else l=new f(e,q);if((p=a[d]=l)&&!p._proxyCallback){p._proxyCallback= -function(){return this._bubbleEvent.call(this,d,p,arguments)};p.on("all",p._proxyCallback,this)}}if(a.hasOwnProperty(d)){b=a[d];f=this.attributes[d];if(b){b.parents=b.parents||[];g.indexOf(b.parents,this)==-1&&b.parents.push(this)}else if(f&&f.parents.length>0)f.parents=g.difference(f.parents,[this])}},this);return r.set.call(this,a,c)},_bubbleEvent:function(a,c,d){var b=d[0].split(":"),n=b[0],f=d[0]=="nested-change",j=d[1],k=d[2],i=-1,h=c._proxyCalls,e,q=g.indexOf(z,n)!==-1;if(!f){g.size(b)>1&&(e= -b[1]);g.indexOf(B,n)!==-1&&(k=j);if(c instanceof m&&q&&j){var l=x(e),p=g.initial(l);(b=c.find(function(a){if(j===a)return true;if(!a)return false;var b=a.get(p);if((b instanceof o||b instanceof m)&&j===b)return true;b=a.get(l);if((b instanceof o||b instanceof m)&&j===b||b instanceof m&&k&&k===b)return true}))&&(i=c.indexOf(b))}e=a+(i!==-1&&(n==="change"||e)?"["+i+"]":"")+(e?"."+e:"");if(/\[\*\]/g.test(e))return this;b=e.replace(/\[\d+\]/g,"[*]");i=[];i.push.apply(i,d);i[0]=n+":"+e;h=c._proxyCalls= -h||{};if(this._isEventAvailable.call(this,h,e))return this;h[e]=true;if("change"===n){this._previousAttributes[a]=c._previousAttributes;this.changed[a]=c}this.trigger.apply(this,i);"change"===n&&this.get(e)!=d[2]&&this.trigger.apply(this,["nested-change",e,d[1]]);h&&e&&delete h[e];if(e!==b){i[0]=n+":"+b;this.trigger.apply(this,i)}return this}},_isEventAvailable:function(a,c){return g.find(a,function(a,b){return c.indexOf(b,c.length-b.length)!==-1})},_createCollection:function(a){var c=a;g.isString(c)&& -(c=t(c));if(c&&c.prototype instanceof o){a=new m;a.model=c}else throw Error("type must inherit from Backbone.AssociatedModel");return a},_processPendingEvents:function(){if(!this.visited){this.visited=true;this._deferEvents=false;g.each(this._pendingEvents,function(a){a.c.trigger.apply(a.c,a.a)});this._pendingEvents=[];g.each(this.relations,function(a){(a=this.attributes[a.key])&&a._processPendingEvents()},this);delete this.visited}},trigger:function(a){if(this._deferEvents){this._pendingEvents=this._pendingEvents|| -[];this._pendingEvents.push({c:this,a:arguments})}else r.trigger.apply(this,arguments)},toJSON:function(a){var c,d;if(!this.visited){this.visited=true;c=r.toJSON.apply(this,arguments);this.relations&&g.each(this.relations,function(b){var h=this.attributes[b.key];if(h){d=h.toJSON(a);c[b.key]=g.isArray(d)?g.compact(d):d}},this);delete this.visited}return c},clone:function(){return new this.constructor(this.toJSON())},_getAttr:function(a){var c=this,a=x(a),d,b;if(!(g.size(a)<1)){for(b=0;bh.size(a))){for(b=0;b backbone-associations.js

backbone-associations.js

Backbone-associations.js 0.5.0

+ -

(c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd. + + + backbone-associations.js + + + + + +

+
+ +
    + +
  • +
    +

    backbone-associations.js

    +
    +
  • + + + +
  • +
    + +
    + +
    +

    Backbone-associations.js 0.5.1

    +

    (c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd. Backbone-associations may be freely distributed under the MIT license. For all details and documentation: - https://github.com/dhruvaray/backbone-associations/

Initial Setup

(function () {
-    "use strict";

Save a reference to the global object (window in the browser, exports -on the server).

    var root = this;

The top-level namespace. All public Backbone classes and modules will be attached to this. -Exported for the browser and CommonJS.

    var _, Backbone, BackboneModel, BackboneCollection, ModelProto,
-        CollectionProto, defaultEvents, AssociatedModel, pathChecker,
-        collectionEvents;
-
-    if (typeof window === 'undefined') {
-        _ = require('underscore');
-        Backbone = require('backbone');
-        if (typeof exports !== 'undefined') {
-            exports = module.exports = Backbone;
-        }
-    } else {
-        _ = root._;
-        Backbone = root.Backbone;
-    }

Create local reference Model prototype.

    BackboneModel = Backbone.Model;
-    BackboneCollection = Backbone.Collection;
-    ModelProto = BackboneModel.prototype;
-    CollectionProto = BackboneCollection.prototype;
-    pathChecker = /[\.\[\]]+/g;

Built-in Backbone events.

    defaultEvents = ["change", "add", "remove", "reset", "sort", "destroy"];
-    collectionEvents = ["reset", "sort"];
-
-    Backbone.Associations = {
-        VERSION:"0.4.2"
-    };

Backbone.AssociatedModel

Add Many and One relations to Backbone Object.

    Backbone.Associations.Many = Backbone.Many = "Many";
-    Backbone.Associations.One = Backbone.One = "One";

Define AssociatedModel (Extends Backbone.Model).

    AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({

Define relations with Associated Model.

        relations:undefined,

Define Model property which can keep track of already fired events, -and prevent redundant event to be triggered in case of cyclic model graphs.

        _proxyCalls:undefined,

Get the value of an attribute.

        get:function (attr) {
-            var obj = ModelProto.get.call(this, attr);
-            return obj ? obj : this._getAttr.apply(this, arguments);
-        },

Set a hash of model attributes on the Backbone Model.

        set:function (key, value, options) {
-            var attributes, result;

Duplicate backbone's behavior to allow separate key/value parameters, -instead of a single 'attributes' object.

            if (_.isObject(key) || key == null) {
-                attributes = key;
-                options = value;
-            } else {
-                attributes = {};
-                attributes[key] = value;
-            }
-            result = this._set(attributes, options);

Trigger events which have been blocked until the entire object graph is updated.

            this._processPendingEvents();
-            return result;
-
-        },

Works with an attribute hash and options + fully qualified paths

        _set:function (attributes, options) {
-            var attr, modelMap, modelId, obj, result = this;
-            if (!attributes) return this;
-            for (attr in attributes) {

Create a map for each unique object whose attributes we want to set

                modelMap || (modelMap = {});
-                if (attr.match(pathChecker)) {
-                    var pathTokens = getPathArray(attr), initials = _.initial(pathTokens),
-                        last = pathTokens[pathTokens.length - 1],
-                        parentModel = this.get(initials);
-                    if (parentModel instanceof AssociatedModel) {
-                        obj = modelMap[parentModel.cid] || (modelMap[parentModel.cid] = {'model':parentModel, 'data':{}});
-                        obj.data[last] = attributes[attr];
-                    }
-                } else {
-                    obj = modelMap[this.cid] || (modelMap[this.cid] = {'model':this, 'data':{}});
-                    obj.data[attr] = attributes[attr];
-                }
-            }
-
-            if (modelMap) {
-                for (modelId in modelMap) {
-                    obj = modelMap[modelId];
-                    this._setAttr.call(obj.model, obj.data, options) || (result = false);
-
-                }
-            } else {
-                result = this._setAttr.call(this, attributes, options);
-            }
-            return result;
-
-        },

Set a hash of model attributes on the object, + https://github.com/dhruvaray/backbone-associations/

+

Initial Setup

+ + + + + + +
  • +
    + +
    + +
    + +
    + +
    (function () {
    +    "use strict";
    + +
  • + + +
  • +
    + +
    + +
    +

    Save a reference to the global object (window in the browser, exports +on the server).

    + +
    + +
        var root = this;
    + +
  • + + +
  • +
    + +
    + +
    +

    The top-level namespace. All public Backbone classes and modules will be attached to this. +Exported for the browser and CommonJS.

    + +
    + +
        var _, Backbone, BackboneModel, BackboneCollection, ModelProto,
    +        CollectionProto, defaultEvents, AssociatedModel, pathChecker,
    +        collectionEvents;
    +
    +    if (typeof window === 'undefined') {
    +        _ = require('underscore');
    +        Backbone = require('backbone');
    +        if (typeof exports !== 'undefined') {
    +            exports = module.exports = Backbone;
    +        }
    +    } else {
    +        _ = root._;
    +        Backbone = root.Backbone;
    +    }
    + +
  • + + +
  • +
    + +
    + +
    +

    Create local reference Model prototype.

    + +
    + +
        BackboneModel = Backbone.Model;
    +    BackboneCollection = Backbone.Collection;
    +    ModelProto = BackboneModel.prototype;
    +    CollectionProto = BackboneCollection.prototype;
    +    pathChecker = /[\.\[\]]+/g;
    + +
  • + + +
  • +
    + +
    + +
    +

    Built-in Backbone events.

    + +
    + +
        defaultEvents = ["change", "add", "remove", "reset", "sort", "destroy"];
    +    collectionEvents = ["reset", "sort"];
    +
    +    Backbone.Associations = {
    +        VERSION:"0.5.1"
    +    };
    + +
  • + + +
  • +
    + +
    + +
    +

    Backbone.AssociatedModel

    + +
    + +
  • + + +
  • +
    + +
    + +
    +

    Add Many and One relations to Backbone Object.

    + +
    + +
        Backbone.Associations.Many = Backbone.Many = "Many";
    +    Backbone.Associations.One = Backbone.One = "One";
    +    Backbone.Associations.Self = Backbone.Self = "Self";
    + +
  • + + +
  • +
    + +
    + +
    +

    Define AssociatedModel (Extends Backbone.Model).

    + +
    + +
        AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({
    + +
  • + + +
  • +
    + +
    + +
    +

    Define relations with Associated Model.

    + +
    + +
            relations:undefined,
    + +
  • + + +
  • +
    + +
    + +
    +

    Define Model property which can keep track of already fired events, +and prevent redundant event to be triggered in case of cyclic model graphs.

    + +
    + +
            _proxyCalls:undefined,
    + +
  • + + +
  • +
    + +
    + +
    +

    Get the value of an attribute.

    + +
    + +
            get:function (attr) {
    +            var obj = ModelProto.get.call(this, attr);
    +            return obj ? obj : this._getAttr.apply(this, arguments);
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Set a hash of model attributes on the Backbone Model.

    + +
    + +
            set:function (key, value, options) {
    +            var attributes, result;
    + +
  • + + +
  • +
    + +
    + +
    +

    Duplicate backbone's behavior to allow separate key/value parameters, +instead of a single 'attributes' object.

    + +
    + +
                if (_.isObject(key) || key == null) {
    +                attributes = key;
    +                options = value;
    +            } else {
    +                attributes = {};
    +                attributes[key] = value;
    +            }
    +            result = this._set(attributes, options);
    + +
  • + + +
  • +
    + +
    + +
    +

    Trigger events which have been blocked until the entire object graph is updated.

    + +
    + +
                this._processPendingEvents();
    +            return result;
    +
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Works with an attribute hash and options + fully qualified paths

    + +
    + +
            _set:function (attributes, options) {
    +            var attr, modelMap, modelId, obj, result = this;
    +            if (!attributes) return this;
    +            for (attr in attributes) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Create a map for each unique object whose attributes we want to set

    + +
    + +
                    modelMap || (modelMap = {});
    +                if (attr.match(pathChecker)) {
    +                    var pathTokens = getPathArray(attr), initials = _.initial(pathTokens),
    +                        last = pathTokens[pathTokens.length - 1],
    +                        parentModel = this.get(initials);
    +                    if (parentModel instanceof AssociatedModel) {
    +                        obj = modelMap[parentModel.cid] || (modelMap[parentModel.cid] = {'model':parentModel, 'data':{}});
    +                        obj.data[last] = attributes[attr];
    +                    }
    +                } else {
    +                    obj = modelMap[this.cid] || (modelMap[this.cid] = {'model':this, 'data':{}});
    +                    obj.data[attr] = attributes[attr];
    +                }
    +            }
    +
    +            if (modelMap) {
    +                for (modelId in modelMap) {
    +                    obj = modelMap[modelId];
    +                    this._setAttr.call(obj.model, obj.data, options) || (result = false);
    +
    +                }
    +            } else {
    +                result = this._setAttr.call(this, attributes, options);
    +            }
    +            return result;
    +
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Set a hash of model attributes on the object, fire Backbone event with options. It maintains relations between models during the set operation. -It also bubbles up child events to the parent.

  •         _setAttr:function (attributes, options) {
    -            var attr;

    Extract attributes and options.

                options || (options = {});
    -            if (options.unset) for (attr in attributes) attributes[attr] = void 0;
    -            this.parents = this.parents || [];
    -
    -            if (this.relations) {

    Iterate over this.relations and set model and collection values -if relations are available.

                    _.each(this.relations, function (relation) {
    -                    var relationKey = relation.key, relatedModel = relation.relatedModel,
    -                        collectionType = relation.collectionType,
    -                        map = relation.map,
    -                        currVal = this.attributes[relationKey],
    -                        idKey = currVal && currVal.idAttribute,
    -                        val, relationOptions, data, relationValue;

    Get class if relation and map is stored as a string.

                        relatedModel && _.isString(relatedModel) && (relatedModel = map2Scope(relatedModel));
    -                    collectionType && _.isString(collectionType) && (collectionType = map2Scope(collectionType));
    -                    map && _.isString(map) && (map = map2Scope(map));

    Merge in options specific to this relation.

                        relationOptions = relation.options ? _.extend({}, relation.options, options) : options;
    -
    -                    if (attributes[relationKey]) {

    Get value of attribute with relation key in val.

                            val = _.result(attributes, relationKey);

    Map val if a transformation function is provided.

                            val = map ? map(val) : val;

    If relation.type is Backbone.Many, -Create Backbone.Collection with passed data and perform Backbone set.

                            if (relation.type === Backbone.Many) {

    collectionType of defined relation should be instance of Backbone.Collection.

                                if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
    -                                throw new Error('collectionType must inherit from Backbone.Collection');
    -                            }
    -
    -                            if (val instanceof BackboneCollection) {
    -                                data = val;
    -                            } else {

    Create a new collection

                                    if (!currVal) {
    -                                    data = collectionType ? new collectionType() : this._createCollection(relatedModel);
    -                                    data.add(val, relationOptions);
    -                                } else {

    Setting this flag will prevent events from firing immediately. That way clients -will not get events until the entire object graph is updated.

                                        currVal._deferEvents = true;

    Use Backbone.Collection's smart set method

                                        currVal.set(val, options);
    -                                    data = currVal;
    -                                }
    -                            }
    -
    -                        } else if (relation.type === Backbone.One && relatedModel) {
    -                            if (val instanceof AssociatedModel) {
    -                                data = val;
    -                            } else {

    Create a new model

                                    if (!currVal) {
    -                                    data = new relatedModel(val, relationOptions);
    -                                } else {

    Is the passed in data for the same key?

                                        if (currVal && val[idKey] && currVal.get(idKey) === val[idKey]) {

    Setting this flag will prevent events from firing immediately. That way clients -will not get events until the entire object graph is updated.

                                            currVal._deferEvents = true;

    Perform the traditional set operation

                                            currVal._set(val, options);
    -                                        data = currVal;
    -                                    } else {
    -                                        data = new relatedModel(val, relationOptions);
    -                                    }
    -                                }
    -                            }
    -
    -                        }
    -
    -                        attributes[relationKey] = data;
    -                        relationValue = data;

    Add proxy events to respective parents. -Only add callback if not defined.

                            if (relationValue && !relationValue._proxyCallback) {
    -                            relationValue._proxyCallback = function () {
    -                                return this._bubbleEvent.call(this, relationKey, relationValue, arguments);
    -                            };
    -                            relationValue.on("all", relationValue._proxyCallback, this);
    -                        }
    -
    -                    }

    Distinguish between the value of undefined versus a set no-op

                        if (attributes.hasOwnProperty(relationKey)) {

    Maintain reverse pointers - a.k.a parents

                            var updated = attributes[relationKey];
    -                        var original = this.attributes[relationKey];
    -                        if (updated) {
    -                            updated.parents = updated.parents || [];
    -                            (_.indexOf(updated.parents, this) == -1) && updated.parents.push(this);
    -                        } else if (original && original.parents.length > 0) {
    -                            original.parents = _.difference(original.parents, [this]);
    -                        }
    -                    }
    -                }, this);
    -            }

    Return results for BackboneModel.set.

                return  ModelProto.set.call(this, attributes, options);
    -        },

    Bubble-up event to parent Model

            _bubbleEvent:function (relationKey, relationValue, eventArguments) {
    -            var args = eventArguments,
    -                opt = args[0].split(":"),
    -                eventType = opt[0],
    -                catch_all = args[0] == "nested-change",
    -                eventObject = args[1],
    -                colObject = args[2],
    -                indexEventObject = -1,
    -                _proxyCalls = relationValue._proxyCalls,
    -                cargs,
    -                eventPath,
    -                basecolEventPath,
    -                isDefaultEvent = _.indexOf(defaultEvents, eventType) !== -1;

    Short circuit the listen in to the nested-graph event

                if (catch_all) return;

    Change the event name to a fully qualified path.

                _.size(opt) > 1 && (eventPath = opt[1]);
    -
    -            if (_.indexOf(collectionEvents, eventType) !== -1) {
    -                colObject = eventObject;
    -            }

    Find the specific object in the collection which has changed.

                if (relationValue instanceof BackboneCollection && isDefaultEvent && eventObject) {
    -                var pathTokens = getPathArray(eventPath),
    -                    initialTokens = _.initial(pathTokens), colModel;
    -
    -                colModel = relationValue.find(function (model) {
    -                    if (eventObject === model) return true;
    -                    if (!model) return false;
    -                    var changedModel = model.get(initialTokens);
    -
    -                    if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection) 
    -                        && eventObject === changedModel)
    -                        return true;
    -
    -                    changedModel = model.get(pathTokens);
    -
    -                    if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection)
    -                        && eventObject === changedModel)
    -                        return true;
    -
    -                    if (changedModel instanceof BackboneCollection && colObject
    -                        && colObject === changedModel)
    -                        return true;
    -                });
    -                colModel && (indexEventObject = relationValue.indexOf(colModel));
    -            }

    Manipulate eventPath.

                eventPath = relationKey + ((indexEventObject !== -1 && (eventType === "change" || eventPath)) ?
    -                "[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : "");

    Short circuit collection * events

                if (/\[\*\]/g.test(eventPath)) return this;
    -            basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]');
    -
    -            cargs = [];
    -            cargs.push.apply(cargs, args);
    -            cargs[0] = eventType + ":" + eventPath;

    If event has been already triggered as result of same source eventPath, -no need to re-trigger event to prevent cycle.

                _proxyCalls = relationValue._proxyCalls = (_proxyCalls || {});
    -            if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this;

    Add eventPath in _proxyCalls to keep track of already triggered event.

                _proxyCalls[eventPath] = true;

    Set up previous attributes correctly.

                if ("change" === eventType) {
    -                this._previousAttributes[relationKey] = relationValue._previousAttributes;
    -                this.changed[relationKey] = relationValue;
    -            }

    Bubble up event to parent model with new changed arguments.

                this.trigger.apply(this, cargs);

    Only fire for change. Not change:attribute

                if ("change" === eventType && this.get(eventPath) != args[2]) {
    -                this.trigger.apply(this, ["nested-change", eventPath, args[1]]);
    -            }

    Remove eventPath from _proxyCalls, +It also bubbles up child events to the parent.

    + + + +
            _setAttr:function (attributes, options) {
    +            var attr;
    + + + + +
  • +
    + +
    + +
    +

    Extract attributes and options.

    + +
    + +
                options || (options = {});
    +            if (options.unset) for (attr in attributes) attributes[attr] = void 0;
    +            this.parents = this.parents || [];
    +
    +            if (this.relations) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Iterate over this.relations and set model and collection values +if relations are available.

    + +
    + +
                    _.each(this.relations, function (relation) {
    +                    var relationKey = relation.key,
    +                        relatedModel = relation.relatedModel,
    +                        collectionType = relation.collectionType,
    +                        map = relation.map,
    +                        currVal = this.attributes[relationKey],
    +                        idKey = currVal && currVal.idAttribute,
    +                        val, relationOptions, data, relationValue, newCtx = false;
    + +
  • + + +
  • +
    + +
    + +
    +

    Call function if relatedModel is implemented as a function

    + +
    + +
                        if (relatedModel && !(relatedModel.prototype instanceof BackboneModel))
    +                        relatedModel = _.isFunction(relatedModel) ?
    +                            relatedModel.call(this, relation, attributes) :
    +                            relatedModel;
    + +
  • + + +
  • +
    + +
    + +
    +

    Get class if relation and map is stored as a string.

    + +
    + +
                        if (relatedModel && _.isString(relatedModel)) {
    +                        relatedModel = (relatedModel === Backbone.Self) ? this.constructor : map2Scope(relatedModel);
    +                    }
    +                    collectionType && _.isString(collectionType) && (collectionType = map2Scope(collectionType));
    +                    map && _.isString(map) && (map = map2Scope(map));
    + +
  • + + +
  • +
    + +
    + +
    +

    Merge in options specific to this relation.

    + +
    + +
                        relationOptions = relation.options ? _.extend({}, relation.options, options) : options;
    +
    +                    if (attributes[relationKey]) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Get value of attribute with relation key in val.

    + +
    + +
                            val = _.result(attributes, relationKey);
    + +
  • + + +
  • +
    + +
    + +
    +

    Map val if a transformation function is provided.

    + +
    + +
                            val = map ? map(val) : val;
    + +
  • + + +
  • +
    + +
    + +
    +

    If relation.type is Backbone.Many, +Create Backbone.Collection with passed data and perform Backbone set.

    + +
    + +
                            if (relation.type === Backbone.Many) {
    + +
  • + + +
  • +
    + +
    + +
    +

    collectionType of defined relation should be instance of Backbone.Collection.

    + +
    + +
                                if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
    +                                throw new Error('collectionType must inherit from Backbone.Collection');
    +                            }
    +
    +                            if (val instanceof BackboneCollection) {
    +                                data = val;
    + +
  • + + +
  • +
    + +
    + +
    +

    Compute whether the context is a new one after this assignment.

    + +
    + +
                                    newCtx = (currVal !== val);
    +                            } else {
    + +
  • + + +
  • +
    + +
    + +
    +

    Create a new collection

    + +
    + +
                                    if (!currVal) {
    +                                    data = collectionType ? new collectionType() : this._createCollection(relatedModel);
    +                                } else {
    +                                    data = currVal;
    + +
  • + + +
  • +
    + +
    + +
    +

    Setting this flag will prevent events from firing immediately. That way clients +will not get events until the entire object graph is updated.

    + +
    + +
                                        data._deferEvents = true;
    +                                }
    + +
  • + + +
  • +
    + +
    + +
    +

    Use Backbone.Collection's reset or smart set method

    + +
    + +
                                    data[relationOptions.reset ? 'reset' : 'set'](val, relationOptions);
    +                            }
    +
    +                        } else if (relation.type === Backbone.One && relatedModel) {
    +                            if (val instanceof AssociatedModel) {
    +                                data = val;
    + +
  • + + +
  • +
    + +
    + +
    +

    Compute whether the context is a new one after this assignment.

    + +
    + +
                                    newCtx = (currVal !== val);
    +                            } else {
    + +
  • + + +
  • +
    + +
    + +
    +

    Create a new model

    + +
    + +
                                    if (!currVal) {
    +                                    data = new relatedModel(val, relationOptions);
    +                                } else {
    + +
  • + + +
  • +
    + +
    + +
    +

    Is the passed in data for the same key?

    + +
    + +
                                        if (currVal && val[idKey] && currVal.get(idKey) === val[idKey]) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Setting this flag will prevent events from firing immediately. That way clients +will not get events until the entire object graph is updated.

    + +
    + +
                                            currVal._deferEvents = true;
    + +
  • + + +
  • +
    + +
    + +
    +

    Perform the traditional set operation

    + +
    + +
                                            currVal._set(val, relationOptions);
    +                                        data = currVal;
    +                                    } else {
    +                                        data = new relatedModel(val, relationOptions);
    +                                    }
    +                                }
    +                            }
    +
    +                        }
    +
    +                        attributes[relationKey] = data;
    +                        relationValue = data;
    + +
  • + + +
  • +
    + +
    + +
    +

    Add proxy events to respective parents. +Only add callback if not defined or new Ctx has been identified.

    + +
    + +
                            if (newCtx || (relationValue && !relationValue._proxyCallback)) {
    +                            relationValue._proxyCallback = function () {
    +                                return this._bubbleEvent.call(this, relationKey, relationValue, arguments);
    +                            };
    +                            relationValue.on("all", relationValue._proxyCallback, this);
    +                        }
    +
    +                    }
    + +
  • + + +
  • +
    + +
    + +
    +

    Distinguish between the value of undefined versus a set no-op

    + +
    + +
                        if (attributes.hasOwnProperty(relationKey)) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Maintain reverse pointers - a.k.a parents

    + +
    + +
                            var updated = attributes[relationKey];
    +                        var original = this.attributes[relationKey];
    +                        if (updated) {
    +                            updated.parents = updated.parents || [];
    +                            (_.indexOf(updated.parents, this) == -1) && updated.parents.push(this);
    +                        } else if (original && original.parents.length > 0) { // New value is undefined
    +                            original.parents = _.difference(original.parents, [this]);
    + +
  • + + +
  • +
    + +
    + +
    +

    Don't bubble to this parent anymore

    + +
    + +
                                original._proxyCallback && original.off("all", original._proxyCallback, this);
    +                        }
    +                    }
    +                }, this);
    +            }
    + +
  • + + +
  • +
    + +
    + +
    +

    Return results for BackboneModel.set.

    + +
    + +
                return  ModelProto.set.call(this, attributes, options);
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Bubble-up event to parent Model

    + +
    + +
            _bubbleEvent:function (relationKey, relationValue, eventArguments) {
    +            var args = eventArguments,
    +                opt = args[0].split(":"),
    +                eventType = opt[0],
    +                catch_all = args[0] == "nested-change",
    +                eventObject = args[1],
    +                colObject = args[2],
    +                indexEventObject = -1,
    +                _proxyCalls = relationValue._proxyCalls,
    +                cargs,
    +                eventPath,
    +                basecolEventPath,
    +                isDefaultEvent = _.indexOf(defaultEvents, eventType) !== -1;
    + +
  • + + +
  • +
    + +
    + +
    +

    Short circuit the listen in to the nested-graph event

    + +
    + +
                if (catch_all) return;
    + +
  • + + +
  • +
    + +
    + +
    +

    Change the event name to a fully qualified path.

    + +
    + +
                _.size(opt) > 1 && (eventPath = opt[1]);
    +
    +            if (_.indexOf(collectionEvents, eventType) !== -1) {
    +                colObject = eventObject;
    +            }
    + +
  • + + +
  • +
    + +
    + +
    +

    Find the specific object in the collection which has changed.

    + +
    + +
                if (relationValue instanceof BackboneCollection && isDefaultEvent && eventObject) {
    +                var pathTokens = getPathArray(eventPath),
    +                    initialTokens = _.initial(pathTokens), colModel;
    +
    +                colModel = relationValue.find(function (model) {
    +                    if (eventObject === model) return true;
    +                    if (!model) return false;
    +                    var changedModel = model.get(initialTokens);
    +
    +                    if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection)
    +                        && eventObject === changedModel)
    +                        return true;
    +
    +                    changedModel = model.get(pathTokens);
    +
    +                    if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection)
    +                        && eventObject === changedModel)
    +                        return true;
    +
    +                    if (changedModel instanceof BackboneCollection && colObject
    +                        && colObject === changedModel)
    +                        return true;
    +                });
    +                colModel && (indexEventObject = relationValue.indexOf(colModel));
    +            }
    + +
  • + + +
  • +
    + +
    + +
    +

    Manipulate eventPath.

    + +
    + +
                eventPath = relationKey + ((indexEventObject !== -1 && (eventType === "change" || eventPath)) ?
    +                "[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : "");
    + +
  • + + +
  • +
    + +
    + +
    +

    Short circuit collection * events

    + +
    + +
                if (/\[\*\]/g.test(eventPath)) return this;
    +            basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]');
    +
    +            cargs = [];
    +            cargs.push.apply(cargs, args);
    +            cargs[0] = eventType + ":" + eventPath;
    + +
  • + + +
  • +
    + +
    + +
    +

    If event has been already triggered as result of same source eventPath, +no need to re-trigger event to prevent cycle.

    + +
    + +
                _proxyCalls = relationValue._proxyCalls = (_proxyCalls || {});
    +            if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this;
    + +
  • + + +
  • +
    + +
    + +
    +

    Add eventPath in _proxyCalls to keep track of already triggered event.

    + +
    + +
                _proxyCalls[eventPath] = true;
    + +
  • + + +
  • +
    + +
    + +
    +

    Set up previous attributes correctly.

    + +
    + +
                if ("change" === eventType) {
    +                this._previousAttributes[relationKey] = relationValue._previousAttributes;
    +                this.changed[relationKey] = relationValue;
    +            }
    + +
  • + + +
  • +
    + +
    + +
    +

    Bubble up event to parent model with new changed arguments.

    + +
    + +
                this.trigger.apply(this, cargs);
    + +
  • + + +
  • +
    + +
    + +
    +

    Only fire for change. Not change:attribute

    + +
    + +
                if ("change" === eventType && this.get(eventPath) != args[2]) {
    +                this.trigger.apply(this, ["nested-change", eventPath, args[1]]);
    +            }
    + +
  • + + +
  • +
    + +
    + +
    +

    Remove eventPath from _proxyCalls, if eventPath and _proxyCalls are available, -which allow event to be triggered on for next operation of set.

  •             if (_proxyCalls && eventPath) delete _proxyCalls[eventPath];

    Create a collection modified event with wild-card

                if (eventPath !== basecolEventPath) {
    -                cargs[0] = eventType + ":" + basecolEventPath;
    -                this.trigger.apply(this, cargs);
    -            }
    -
    -            return this;
    -        },

    Has event been fired from this source. Used to prevent event recursion in cyclic graphs

            _isEventAvailable:function (_proxyCalls, path) {
    -            return _.find(_proxyCalls, function (value, eventKey) {
    -                return path.indexOf(eventKey, path.length - eventKey.length) !== -1;
    -            });
    -        },

    Returns New collection of type relation.relatedModel.

            _createCollection:function (type) {
    -            var collection, relatedModel = type;
    -            _.isString(relatedModel) && (relatedModel = map2Scope(relatedModel));

    Creates new Backbone.Collection and defines model class.

                if (relatedModel && relatedModel.prototype instanceof AssociatedModel) {
    -                collection = new BackboneCollection();
    -                collection.model = relatedModel;
    -            } else {
    -                throw new Error('type must inherit from Backbone.AssociatedModel');
    -            }
    -            return collection;
    -        },

    Process all pending events after the entire object graph has been updated

            _processPendingEvents:function () {
    -            if (!this.visited) {
    -                this.visited = true;
    -
    -                this._deferEvents = false;

    Trigger all pending events

                    _.each(this._pendingEvents, function (e) {
    -                    e.c.trigger.apply(e.c, e.a);
    -                });
    -
    -                this._pendingEvents = [];

    Traverse down the object graph and call process pending events on sub-trees

                    _.each(this.relations, function (relation) {
    -                    var val = this.attributes[relation.key];
    -                    val && val._processPendingEvents();
    -                }, this);
    -
    -                delete this.visited;
    -            }
    -        },

    Override trigger to defer events in the object graph.

            trigger:function (name) {

    Defer event processing

                if (this._deferEvents) {
    -                this._pendingEvents = this._pendingEvents || [];

    Maintain a queue of pending events to trigger after the entire object graph is updated.

                    this._pendingEvents.push({c:this, a:arguments});
    -            } else {
    -                ModelProto.trigger.apply(this, arguments);
    -            }
    -        },

    The JSON representation of the model.

            toJSON:function (options) {
    -            var json, aJson;
    -            if (!this.visited) {
    -                this.visited = true;

    Get json representation from BackboneModel.toJSON.

                    json = ModelProto.toJSON.apply(this, arguments);

    If this.relations is defined, iterate through each relation -and added it's json representation to parents' json representation.

                    if (this.relations) {
    -                    _.each(this.relations, function (relation) {
    -                        var attr = this.attributes[relation.key];
    -                        if (attr) {
    -                            aJson = attr.toJSON(options);
    -                            json[relation.key] = _.isArray(aJson) ? _.compact(aJson) : aJson;
    -                        }
    -                    }, this);
    -                }
    -                delete this.visited;
    -            }
    -            return json;
    -        },

    Create a new model with identical attributes to this one.

            clone:function () {
    -            return new this.constructor(this.toJSON());
    -        },

    Navigate the path to the leaf object in the path to query for the attribute value

            _getAttr:function (path) {
    -
    -            var result = this,

    Tokenize the path

                    attrs = getPathArray(path),
    -                key,
    -                i;
    -            if (_.size(attrs) < 1) return;
    -            for (i = 0; i < attrs.length; i++) {
    -                key = attrs[i];
    -                if (!result) break;

    Navigate the path to get to the result

                    result = result instanceof BackboneCollection
    -                    ? (isNaN(key) ? undefined : result.at(key))
    -                    : result.attributes[key];
    -            }
    -            return result;
    -        }
    -    });
    -
    -    var delimiters = /[^\.\[\]]+/g;

    Tokenize the fully qualified event path

        var getPathArray = function (path) {
    -        if (path === '') return [''];
    -        return _.isString(path) ? (path.match(delimiters)) : path || [];
    -    };
    -
    -    var map2Scope = function (path) {
    -        return _.reduce(path.split('.'), function (memo, elem) {
    -            return memo[elem]
    -        }, root);
    -    };

    Infer the relation from the collection's parents and find the appropriate map for the passed in models

        var map2models = function (parents, target, models) {
    -        var relation;

    Iterate over collection's parents

            _.find(parents, function (parent) {

    Iterate over relations

                relation = _.find(parent.relations, function (rel) {
    -                return parent.get(rel.key) === target;
    -            }, this);
    -            if (relation) return true;//break;
    -        }, this);

    If we found a relation and it has a mapping function

            if (relation && relation.map) {
    -            return relation.map(models)
    -        }
    -        return models;
    -    };
    -
    -    var proxies = {};

    Proxy Backbone collection methods

        _.each(['set', 'remove', 'reset'], function (method) {
    -        proxies[method] = BackboneCollection.prototype[method];
    -
    -        CollectionProto[method] = function (models, options) {

    Short-circuit if this collection doesn't hold AssociatedModels

                if (this.model.prototype instanceof AssociatedModel && this.parents) {

    Find a map function if available and perform a transformation

                    arguments[0] = map2models(this.parents, this, models);
    -            }
    -            return proxies[method].apply(this, arguments);
    -        }
    -    });

    Override trigger to defer events in the object graph.

        proxies['trigger'] = CollectionProto['trigger'];
    -    CollectionProto['trigger'] = function (name) {
    -        if (this._deferEvents) {
    -            this._pendingEvents = this._pendingEvents || [];

    Maintain a queue of pending events to trigger after the entire object graph is updated.

                this._pendingEvents.push({c:this, a:arguments});
    -        } else {
    -            proxies['trigger'].apply(this, arguments);
    -        }
    -    };

    Attach process pending event functionality on collections as well. Re-use from AssociatedModel

        CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents;
    -
    -
    -}).call(this);
    -
    -
    \ No newline at end of file +which allow event to be triggered on for next operation of set.

    + + + +
                if (_proxyCalls && eventPath) delete _proxyCalls[eventPath];
    + + + + +
  • +
    + +
    + +
    +

    Create a collection modified event with wild-card

    + +
    + +
                if (eventPath !== basecolEventPath) {
    +                cargs[0] = eventType + ":" + basecolEventPath;
    +                this.trigger.apply(this, cargs);
    +            }
    +
    +            return this;
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Has event been fired from this source. Used to prevent event recursion in cyclic graphs

    + +
    + +
            _isEventAvailable:function (_proxyCalls, path) {
    +            return _.find(_proxyCalls, function (value, eventKey) {
    +                return path.indexOf(eventKey, path.length - eventKey.length) !== -1;
    +            });
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Returns New collection of type relation.relatedModel.

    + +
    + +
            _createCollection:function (type) {
    +            var collection, relatedModel = type;
    +            _.isString(relatedModel) && (relatedModel = map2Scope(relatedModel));
    + +
  • + + +
  • +
    + +
    + +
    +

    Creates new Backbone.Collection and defines model class.

    + +
    + +
                if (relatedModel && relatedModel.prototype instanceof AssociatedModel) {
    +                collection = new BackboneCollection();
    +                collection.model = relatedModel;
    +            } else {
    +                throw new Error('type must inherit from Backbone.AssociatedModel');
    +            }
    +            return collection;
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Process all pending events after the entire object graph has been updated

    + +
    + +
            _processPendingEvents:function () {
    +            if (!this.visited) {
    +                this.visited = true;
    +
    +                this._deferEvents = false;
    + +
  • + + +
  • +
    + +
    + +
    +

    Trigger all pending events

    + +
    + +
                    _.each(this._pendingEvents, function (e) {
    +                    e.c.trigger.apply(e.c, e.a);
    +                });
    +
    +                this._pendingEvents = [];
    + +
  • + + +
  • +
    + +
    + +
    +

    Traverse down the object graph and call process pending events on sub-trees

    + +
    + +
                    _.each(this.relations, function (relation) {
    +                    var val = this.attributes[relation.key];
    +                    val && val._processPendingEvents();
    +                }, this);
    +
    +                delete this.visited;
    +            }
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Override trigger to defer events in the object graph.

    + +
    + +
            trigger:function (name) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Defer event processing

    + +
    + +
                if (this._deferEvents) {
    +                this._pendingEvents = this._pendingEvents || [];
    + +
  • + + +
  • +
    + +
    + +
    +

    Maintain a queue of pending events to trigger after the entire object graph is updated.

    + +
    + +
                    this._pendingEvents.push({c:this, a:arguments});
    +            } else {
    +                ModelProto.trigger.apply(this, arguments);
    +            }
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    The JSON representation of the model.

    + +
    + +
            toJSON:function (options) {
    +            var json, aJson;
    +            if (!this.visited) {
    +                this.visited = true;
    + +
  • + + +
  • +
    + +
    + +
    +

    Get json representation from BackboneModel.toJSON.

    + +
    + +
                    json = ModelProto.toJSON.apply(this, arguments);
    + +
  • + + +
  • +
    + +
    + +
    +

    If this.relations is defined, iterate through each relation +and added it's json representation to parents' json representation.

    + +
    + +
                    if (this.relations) {
    +                    _.each(this.relations, function (relation) {
    +                        var attr = this.attributes[relation.key];
    +                        if (attr) {
    +                            aJson = attr.toJSON(options);
    +                            json[relation.key] = _.isArray(aJson) ? _.compact(aJson) : aJson;
    +                        }
    +                    }, this);
    +                }
    +                delete this.visited;
    +            }
    +            return json;
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Create a new model with identical attributes to this one.

    + +
    + +
            clone:function () {
    +            return new this.constructor(this.toJSON());
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Call this if you want to set an AssociatedModel to a falsy value like undefined/null directly. +Not calling this will leak memory and have wrong parents. +See test case "parent relations"

    + +
    + +
            cleanup:function () {
    +            _.each(this.relations, function (relation) {
    +                var val = this.attributes[relation.key];
    +                val && (val.parents = _.difference(val.parents, [this]));
    +            }, this);
    +            this.off();
    +        },
    + +
  • + + +
  • +
    + +
    + +
    +

    Navigate the path to the leaf object in the path to query for the attribute value

    + +
    + +
            _getAttr:function (path) {
    +
    +            var result = this,
    + +
  • + + +
  • +
    + +
    + +
    +

    Tokenize the path

    + +
    + +
                    attrs = getPathArray(path),
    +                key,
    +                i;
    +            if (_.size(attrs) < 1) return;
    +            for (i = 0; i < attrs.length; i++) {
    +                key = attrs[i];
    +                if (!result) break;
    + +
  • + + +
  • +
    + +
    + +
    +

    Navigate the path to get to the result

    + +
    + +
                    result = result instanceof BackboneCollection
    +                    ? (isNaN(key) ? undefined : result.at(key))
    +                    : result.attributes[key];
    +            }
    +            return result;
    +        }
    +    });
    +
    +    var delimiters = /[^\.\[\]]+/g;
    + +
  • + + +
  • +
    + +
    + +
    +

    Tokenize the fully qualified event path

    + +
    + +
        var getPathArray = function (path) {
    +        if (path === '') return [''];
    +        return _.isString(path) ? (path.match(delimiters)) : path || [];
    +    };
    +
    +    var map2Scope = function (path) {
    +        return _.reduce(path.split('.'), function (memo, elem) {
    +            return memo[elem]
    +        }, root);
    +    };
    + +
  • + + +
  • +
    + +
    + +
    +

    Infer the relation from the collection's parents and find the appropriate map for the passed in models

    + +
    + +
        var map2models = function (parents, target, models) {
    +        var relation;
    + +
  • + + +
  • +
    + +
    + +
    +

    Iterate over collection's parents

    + +
    + +
            _.find(parents, function (parent) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Iterate over relations

    + +
    + +
                relation = _.find(parent.relations, function (rel) {
    +                return parent.get(rel.key) === target;
    +            }, this);
    +            if (relation) return true;//break;
    +        }, this);
    + +
  • + + +
  • +
    + +
    + +
    +

    If we found a relation and it has a mapping function

    + +
    + +
            if (relation && relation.map) {
    +            return relation.map(models)
    +        }
    +        return models;
    +    };
    +
    +    var proxies = {};
    + +
  • + + +
  • +
    + +
    + +
    +

    Proxy Backbone collection methods

    + +
    + +
        _.each(['set', 'remove', 'reset'], function (method) {
    +        proxies[method] = BackboneCollection.prototype[method];
    +
    +        CollectionProto[method] = function (models, options) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Short-circuit if this collection doesn't hold AssociatedModels

    + +
    + +
                if (this.model.prototype instanceof AssociatedModel && this.parents) {
    + +
  • + + +
  • +
    + +
    + +
    +

    Find a map function if available and perform a transformation

    + +
    + +
                    arguments[0] = map2models(this.parents, this, models);
    +            }
    +            return proxies[method].apply(this, arguments);
    +        }
    +    });
    + +
  • + + +
  • +
    + +
    + +
    +

    Override trigger to defer events in the object graph.

    + +
    + +
        proxies['trigger'] = CollectionProto['trigger'];
    +    CollectionProto['trigger'] = function (name) {
    +        if (this._deferEvents) {
    +            this._pendingEvents = this._pendingEvents || [];
    + +
  • + + +
  • +
    + +
    + +
    +

    Maintain a queue of pending events to trigger after the entire object graph is updated.

    + +
    + +
                this._pendingEvents.push({c:this, a:arguments});
    +        } else {
    +            proxies['trigger'].apply(this, arguments);
    +        }
    +    };
    + +
  • + + +
  • +
    + +
    + +
    +

    Attach process pending event functionality on collections as well. Re-use from AssociatedModel

    + +
    + +
        CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents;
    +
    +
    +}).call(this);
    + +
  • + + + + + diff --git a/docs/docco.css b/docs/docco.css index 04cc7ec..1d56071 100755 --- a/docs/docco.css +++ b/docs/docco.css @@ -1,192 +1,428 @@ /*--------------------- Layout and Typography ----------------------------*/ +html { height: 100%; } body { font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; - font-size: 15px; - line-height: 22px; + font-size: 14px; + line-height: 16px; color: #252519; margin: 0; padding: 0; + height:100%; } +#container { min-height: 100%; } + a { color: #261a3b; } - a:visited { - color: #261a3b; - } -p { - margin: 0 0 15px 0; + +a:visited { + color: #261a3b; } + +p, ul, ol { + margin: 0 0 15px; +} + h1, h2, h3, h4, h5, h6 { - margin: 0px 0 15px 0; + margin: 30px 0 15px 0; } - h1 { - margin-top: 40px; - } + +h1 { + margin-top: 40px; +} + hr { - border: 0 none; - border-top: 1px solid #e5e5ee; - height: 1px; - margin: 20px 0; + border: 0 none; + border-top: 1px solid #e5e5ee; + height: 1px; + margin: 20px 0; } -#container { - position: relative; + +pre, tt, code { + font-size: 12px; line-height: 16px; + font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; + margin: 0; padding: 0; } -#background { - position: fixed; - top: 0; left: 525px; right: 0; bottom: 0; - background: #f5f5ff; - border-left: 1px solid #e5e5ee; - z-index: -1; + +ul.sections { + list-style: none; + padding:0 0 5px 0;; + margin:0; +} + +/* + Force border-box so that % widths fit the parent + container without overlap because of margin/padding. + + More Info : http://www.quirksmode.org/css/box.html +*/ +ul.sections > li > div { + -moz-box-sizing: border-box; /* firefox */ + -ms-box-sizing: border-box; /* ie */ + -webkit-box-sizing: border-box; /* webkit */ + -khtml-box-sizing: border-box; /* konqueror */ + box-sizing: border-box; /* css3 */ } + + +/*---------------------- Jump Page -----------------------------*/ #jump_to, #jump_page { + margin: 0; background: white; -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; - font: 10px Arial; - text-transform: uppercase; + font: 16px Arial; cursor: pointer; text-align: right; + list-style: none; +} + +#jump_to a { + text-decoration: none; } + +#jump_to a.large { + display: none; +} +#jump_to a.small { + font-size: 22px; + font-weight: bold; + color: #676767; +} + #jump_to, #jump_wrapper { position: fixed; right: 0; top: 0; - padding: 5px 10px; + padding: 10px 15px; + margin:0; } + +#jump_wrapper { + display: none; + padding:0; +} + +#jump_to:hover #jump_wrapper { + display: block; +} + +#jump_page { + padding: 5px 0 3px; + margin: 0 0 25px 25px; +} + +#jump_page .source { + display: block; + padding: 15px; + text-decoration: none; + border-top: 1px solid #eee; +} + +#jump_page .source:hover { + background: #f5f5ff; +} + +#jump_page .source:first-child { +} + +/*---------------------- Low resolutions (> 320px) ---------------------*/ +@media only screen and (min-width: 320px) { + .pilwrap { display: none; } + + ul.sections > li > div { + display: block; + padding:5px 10px 0 10px; + } + + ul.sections > li > div.annotation { + background: #fff; + } + + ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { + padding-left: 30px; + } + + ul.sections > li > div.content { + background: #f5f5ff; + overflow-x:auto; + -webkit-box-shadow: inset 0 0 5px #e5e5ee; + box-shadow: inset 0 0 5px #e5e5ee; + border: 1px solid #dedede; + margin:5px 10px 5px 10px; + padding-bottom: 5px; + } + + ul.sections > li > div.annotation pre { + margin: 7px 0 7px; + padding-left: 15px; + } + + ul.sections > li > div.annotation p tt, .annotation code { + background: #f8f8ff; + border: 1px solid #dedede; + font-size: 12px; + padding: 0 0.2em; + } +} + +/*---------------------- (> 481px) ---------------------*/ +@media only screen and (min-width: 481px) { + #container { + position: relative; + } + body { + background-color: #F5F5FF; + font-size: 15px; + line-height: 22px; + } + pre, tt, code { + line-height: 18px; + } + + #jump_to { + padding: 5px 10px; + } #jump_wrapper { padding: 0; + } + #jump_to, #jump_page { + font: 10px Arial; + text-transform: uppercase; + } + #jump_page .source { + padding: 5px 10px; + } + #jump_to a.large { + display: inline-block; + } + #jump_to a.small { display: none; } - #jump_to:hover #jump_wrapper { - display: block; - } - #jump_page { - padding: 5px 0 3px; - margin: 0 0 25px 25px; - } - #jump_page .source { - display: block; - padding: 5px 10px; - text-decoration: none; - border-top: 1px solid #eee; - } - #jump_page .source:hover { - background: #f5f5ff; - } - #jump_page .source:first-child { - } -table td { - border: 0; - outline: 0; -} - td.docs, th.docs { - max-width: 450px; - min-width: 450px; + + + + #background { + position: absolute; + top: 0; bottom: 0; + width: 350px; + background: #ffffff; + border-right: 1px solid #e5e5ee; + z-index: -1; + } + + ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { + padding-left: 40px; + } + + ul.sections > li { + white-space: nowrap; + } + + ul.sections > li > div { + display: inline-block; + } + + ul.sections > li > div.annotation { + max-width: 350px; + min-width: 350px; min-height: 5px; - padding: 10px 25px 1px 50px; + padding: 13px; overflow-x: hidden; + white-space: normal; vertical-align: top; text-align: left; } - .docs pre { - margin: 15px 0 15px; - padding-left: 15px; - } - .docs p tt, .docs p code { - background: #f8f8ff; - border: 1px solid #dedede; - font-size: 12px; - padding: 0 0.2em; - } - .pilwrap { - position: relative; - } - .pilcrow { - font: 12px Arial; - text-decoration: none; - color: #454545; - position: absolute; - top: 3px; left: -20px; - padding: 1px 2px; - opacity: 0; - -webkit-transition: opacity 0.2s linear; - } - td.docs:hover .pilcrow { - opacity: 1; - } - td.code, th.code { - padding: 14px 15px 16px 25px; - width: 100%; + ul.sections > li > div.annotation pre { + margin: 15px 0 15px; + padding-left: 15px; + } + + ul.sections > li > div.content { + padding: 13px; vertical-align: top; background: #f5f5ff; - border-left: 1px solid #e5e5ee; + border: none; + -webkit-box-shadow: none; + box-shadow: none; } - pre, tt, code { - font-size: 12px; line-height: 18px; - font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; - margin: 0; padding: 0; + + .pilwrap { + position: relative; + display: inline; + } + + .pilcrow { + font: 12px Arial; + text-decoration: none; + color: #454545; + position: absolute; + top: 3px; left: -20px; + padding: 1px 2px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + } + .for-h1 .pilcrow { + top: 47px; + } + .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { + top: 35px; } + ul.sections > li > div.annotation:hover .pilcrow { + opacity: 1; + } +} + +/*---------------------- (> 1025px) ---------------------*/ +@media only screen and (min-width: 1025px) { + + #background { + width: 525px; + } + ul.sections > li > div.annotation { + max-width: 525px; + min-width: 525px; + padding: 10px 25px 1px 50px; + } + ul.sections > li > div.content { + padding: 9px 15px 16px 25px; + } +} /*---------------------- Syntax Highlighting -----------------------------*/ + td.linenos { background-color: #f0f0f0; padding-right: 10px; } span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } -body .hll { background-color: #ffffcc } -body .c { color: #408080; font-style: italic } /* Comment */ -body .err { border: 1px solid #FF0000 } /* Error */ -body .k { color: #954121 } /* Keyword */ -body .o { color: #666666 } /* Operator */ -body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ -body .cp { color: #BC7A00 } /* Comment.Preproc */ -body .c1 { color: #408080; font-style: italic } /* Comment.Single */ -body .cs { color: #408080; font-style: italic } /* Comment.Special */ -body .gd { color: #A00000 } /* Generic.Deleted */ -body .ge { font-style: italic } /* Generic.Emph */ -body .gr { color: #FF0000 } /* Generic.Error */ -body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -body .gi { color: #00A000 } /* Generic.Inserted */ -body .go { color: #808080 } /* Generic.Output */ -body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -body .gs { font-weight: bold } /* Generic.Strong */ -body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -body .gt { color: #0040D0 } /* Generic.Traceback */ -body .kc { color: #954121 } /* Keyword.Constant */ -body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ -body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ -body .kp { color: #954121 } /* Keyword.Pseudo */ -body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ -body .kt { color: #B00040 } /* Keyword.Type */ -body .m { color: #666666 } /* Literal.Number */ -body .s { color: #219161 } /* Literal.String */ -body .na { color: #7D9029 } /* Name.Attribute */ -body .nb { color: #954121 } /* Name.Builtin */ -body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -body .no { color: #880000 } /* Name.Constant */ -body .nd { color: #AA22FF } /* Name.Decorator */ -body .ni { color: #999999; font-weight: bold } /* Name.Entity */ -body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -body .nf { color: #0000FF } /* Name.Function */ -body .nl { color: #A0A000 } /* Name.Label */ -body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -body .nt { color: #954121; font-weight: bold } /* Name.Tag */ -body .nv { color: #19469D } /* Name.Variable */ -body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -body .w { color: #bbbbbb } /* Text.Whitespace */ -body .mf { color: #666666 } /* Literal.Number.Float */ -body .mh { color: #666666 } /* Literal.Number.Hex */ -body .mi { color: #666666 } /* Literal.Number.Integer */ -body .mo { color: #666666 } /* Literal.Number.Oct */ -body .sb { color: #219161 } /* Literal.String.Backtick */ -body .sc { color: #219161 } /* Literal.String.Char */ -body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ -body .s2 { color: #219161 } /* Literal.String.Double */ -body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -body .sh { color: #219161 } /* Literal.String.Heredoc */ -body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -body .sx { color: #954121 } /* Literal.String.Other */ -body .sr { color: #BB6688 } /* Literal.String.Regex */ -body .s1 { color: #219161 } /* Literal.String.Single */ -body .ss { color: #19469D } /* Literal.String.Symbol */ -body .bp { color: #954121 } /* Name.Builtin.Pseudo */ -body .vc { color: #19469D } /* Name.Variable.Class */ -body .vg { color: #19469D } /* Name.Variable.Global */ -body .vi { color: #19469D } /* Name.Variable.Instance */ -body .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file +/* + +github.com style (c) Vasily Polovnyov + +*/ + +pre code { + display: block; padding: 0.5em; + color: #000; + background: #f8f8ff +} + +pre .comment, +pre .template_comment, +pre .diff .header, +pre .javadoc { + color: #408080; + font-style: italic +} + +pre .keyword, +pre .assignment, +pre .literal, +pre .css .rule .keyword, +pre .winutils, +pre .javascript .title, +pre .lisp .title, +pre .subst { + color: #954121; + /*font-weight: bold*/ +} + +pre .number, +pre .hexcolor { + color: #40a070 +} + +pre .string, +pre .tag .value, +pre .phpdoc, +pre .tex .formula { + color: #219161; +} + +pre .title, +pre .id { + color: #19469D; +} +pre .params { + color: #00F; +} + +pre .javascript .title, +pre .lisp .title, +pre .subst { + font-weight: normal +} + +pre .class .title, +pre .haskell .label, +pre .tex .command { + color: #458; + font-weight: bold +} + +pre .tag, +pre .tag .title, +pre .rules .property, +pre .django .tag .keyword { + color: #000080; + font-weight: normal +} + +pre .attribute, +pre .variable, +pre .instancevar, +pre .lisp .body { + color: #008080 +} + +pre .regexp { + color: #B68 +} + +pre .class { + color: #458; + font-weight: bold +} + +pre .symbol, +pre .ruby .symbol .string, +pre .ruby .symbol .keyword, +pre .ruby .symbol .keymethods, +pre .lisp .keyword, +pre .tex .special, +pre .input_number { + color: #990073 +} + +pre .builtin, +pre .constructor, +pre .built_in, +pre .lisp .title { + color: #0086b3 +} + +pre .preprocessor, +pre .pi, +pre .doctype, +pre .shebang, +pre .cdata { + color: #999; + font-weight: bold +} + +pre .deletion { + background: #fdd +} + +pre .addition { + background: #dfd +} + +pre .diff .change { + background: #0086b3 +} + +pre .chunk { + color: #aaa +} + +pre .tex .formula { + opacity: 0.5; +} diff --git a/docs/public/fonts/aller-bold.eot b/docs/public/fonts/aller-bold.eot new file mode 100644 index 0000000..1b32532 Binary files /dev/null and b/docs/public/fonts/aller-bold.eot differ diff --git a/docs/public/fonts/aller-bold.ttf b/docs/public/fonts/aller-bold.ttf new file mode 100644 index 0000000..dc4cc9c Binary files /dev/null and b/docs/public/fonts/aller-bold.ttf differ diff --git a/docs/public/fonts/aller-bold.woff b/docs/public/fonts/aller-bold.woff new file mode 100644 index 0000000..fa16fd0 Binary files /dev/null and b/docs/public/fonts/aller-bold.woff differ diff --git a/docs/public/fonts/aller-light.eot b/docs/public/fonts/aller-light.eot new file mode 100644 index 0000000..40bd654 Binary files /dev/null and b/docs/public/fonts/aller-light.eot differ diff --git a/docs/public/fonts/aller-light.ttf b/docs/public/fonts/aller-light.ttf new file mode 100644 index 0000000..c2c7290 Binary files /dev/null and b/docs/public/fonts/aller-light.ttf differ diff --git a/docs/public/fonts/aller-light.woff b/docs/public/fonts/aller-light.woff new file mode 100644 index 0000000..81a09d1 Binary files /dev/null and b/docs/public/fonts/aller-light.woff differ diff --git a/docs/public/fonts/fleurons.eot b/docs/public/fonts/fleurons.eot new file mode 100644 index 0000000..26189bb Binary files /dev/null and b/docs/public/fonts/fleurons.eot differ diff --git a/docs/public/fonts/fleurons.ttf b/docs/public/fonts/fleurons.ttf new file mode 100644 index 0000000..7b1b017 Binary files /dev/null and b/docs/public/fonts/fleurons.ttf differ diff --git a/docs/public/fonts/fleurons.woff b/docs/public/fonts/fleurons.woff new file mode 100644 index 0000000..10b7e1a Binary files /dev/null and b/docs/public/fonts/fleurons.woff differ diff --git a/docs/public/fonts/novecento-bold.eot b/docs/public/fonts/novecento-bold.eot new file mode 100644 index 0000000..98a9a7f Binary files /dev/null and b/docs/public/fonts/novecento-bold.eot differ diff --git a/docs/public/fonts/novecento-bold.ttf b/docs/public/fonts/novecento-bold.ttf new file mode 100644 index 0000000..2af39b0 Binary files /dev/null and b/docs/public/fonts/novecento-bold.ttf differ diff --git a/docs/public/fonts/novecento-bold.woff b/docs/public/fonts/novecento-bold.woff new file mode 100644 index 0000000..de558b5 Binary files /dev/null and b/docs/public/fonts/novecento-bold.woff differ diff --git a/docs/public/images/gray.png b/docs/public/images/gray.png new file mode 100644 index 0000000..6eb6669 Binary files /dev/null and b/docs/public/images/gray.png differ diff --git a/docs/public/stylesheets/normalize.css b/docs/public/stylesheets/normalize.css new file mode 100644 index 0000000..73abb76 --- /dev/null +++ b/docs/public/stylesheets/normalize.css @@ -0,0 +1,375 @@ +/*! normalize.css v2.0.1 | MIT License | git.io/normalize */ + +/* ========================================================================== + HTML5 display definitions + ========================================================================== */ + +/* + * Corrects `block` display not defined in IE 8/9. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects `inline-block` display not defined in IE 8/9. + */ + +audio, +canvas, +video { + display: inline-block; +} + +/* + * Prevents modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/* + * Addresses styling for `hidden` attribute not present in IE 8/9. + */ + +[hidden] { + display: none; +} + +/* ========================================================================== + Base + ========================================================================== */ + +/* + * 1. Sets default font family to sans-serif. + * 2. Prevents iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -ms-text-size-adjust: 100%; /* 2 */ +} + +/* + * Removes default margin. + */ + +body { + margin: 0; +} + +/* ========================================================================== + Links + ========================================================================== */ + +/* + * Addresses `outline` inconsistency between Chrome and other browsers. + */ + +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* ========================================================================== + Typography + ========================================================================== */ + +/* + * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, + * Safari 5, and Chrome. + */ + +h1 { + font-size: 2em; +} + +/* + * Addresses styling not present in IE 8/9, Safari 5, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/* + * Addresses styling not present in Safari 5 and Chrome. + */ + +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + + +/* + * Corrects font family set oddly in Safari 5 and Chrome. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, serif; + font-size: 1em; +} + +/* + * Improves readability of pre-formatted text in all browsers. + */ + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* + * Sets consistent quote types. + */ + +q { + quotes: "\201C" "\201D" "\2018" "\2019"; +} + +/* + * Addresses inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/* + * Prevents `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ========================================================================== + Embedded content + ========================================================================== */ + +/* + * Removes border when inside `a` element in IE 8/9. + */ + +img { + border: 0; +} + +/* + * Corrects overflow displayed oddly in IE 9. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* ========================================================================== + Figures + ========================================================================== */ + +/* + * Addresses margin not present in IE 8/9 and Safari 5. + */ + +figure { + margin: 0; +} + +/* ========================================================================== + Forms + ========================================================================== */ + +/* + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE 8/9. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/* + * 1. Corrects font family not being inherited in all browsers. + * 2. Corrects font size not being inherited in all browsers. + * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome + */ + +button, +input, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 2 */ + margin: 0; /* 3 */ +} + +/* + * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +button, +input { + line-height: normal; +} + +/* + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Corrects inability to style clickable `input` types in iOS. + * 3. Improves usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/* + * Re-set default cursor for disabled elements. + */ + +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to `content-box` in IE 8/9. + * 2. Removes excess padding in IE 8/9. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/* + * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. + * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/* + * Removes inner padding and search cancel button in Safari 5 and Chrome + * on OS X. + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* + * Removes inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE 8/9. + * 2. Improves readability and alignment in all browsers. + */ + +textarea { + overflow: auto; /* 1 */ + vertical-align: top; /* 2 */ +} + +/* ========================================================================== + Tables + ========================================================================== */ + +/* + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/package.json b/package.json index bf0cdbb..efbf60c 100755 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "test":"phantomjs test/lib/runner.js test/test-suite.html?noglobals=true" }, "main":"backbone-associations.js", - "version":"0.5.0", + "version":"0.5.1", "repository":"git://github.com/dhruvaray/backbone-associations.git", "licenses": [ {