--- /dev/null
+/**
+ * Backbone-Nested 1.1.0 - An extension of Backbone.js that keeps track of nested attributes
+ *
+ * http://afeld.github.com/backbone-nested/
+ *
+ * Copyright (c) 2011-2012 Aidan Feldman
+ * MIT Licensed (LICENSE)
+ */
+/*global Backbone, _, $ */
+(function(){
+ 'use strict';
+
+ Backbone.NestedModel = Backbone.Model.extend({
+
+ get: function(attrStrOrPath, opts){
+ opts = opts || {};
+
+ var attrPath = Backbone.NestedModel.attrPath(attrStrOrPath),
+ childAttr = attrPath[0],
+ result = Backbone.NestedModel.__super__.get.call(this, childAttr);
+
+ // walk through the child attributes
+ for (var i = 1; i < attrPath.length; i++){
+ if (!result){
+ // value not present
+ break;
+ }
+ childAttr = attrPath[i];
+ result = result[childAttr];
+ }
+
+ // check if the result is an Object, Array, etc.
+ if (!opts.silent && _.isObject(result) && window.console){
+ window.console.log("Backbone-Nested syntax is preferred for accesing values of attribute '" + attrStrOrPath + "'.");
+ }
+ // else it's a leaf
+
+ return result;
+ },
+
+ has: function(attr){
+ // for some reason this is not how Backbone.Model is implemented - it accesses the attributes object directly
+ var result = this.get(attr, {silent: true});
+ return !(result === null || _.isUndefined(result));
+ },
+
+ set: function(key, value, opts){
+ var attrs;
+ if (_.isObject(key) || key == null) {
+ attrs = key;
+ opts = value;
+ } else {
+ attrs = {};
+ attrs[key] = value;
+ }
+ opts = opts || {};
+
+ var newAttrs = Backbone.NestedModel.deepClone(this.attributes),
+ attrVal, attrPath, attrObj;
+
+ for (var attrStr in attrs){
+ attrPath = Backbone.NestedModel.attrPath(attrStr);
+ attrObj = Backbone.NestedModel.createAttrObj(attrPath, attrs[attrStr]);
+
+ this._mergeAttrs(newAttrs, attrObj, opts);
+ }
+
+ return Backbone.NestedModel.__super__.set.call(this, newAttrs, opts);
+ },
+
+ unset: function(attrStr, opts){
+ opts = _.extend({}, opts, {unset: true});
+ this.set(attrStr, null, opts);
+
+ return this;
+ },
+
+ add: function(attrStr, value, opts){
+ var current = this.get(attrStr, {silent: true});
+ this.set(attrStr + '[' + current.length + ']', value, opts);
+ },
+
+ remove: function(attrStr, opts){
+ opts = opts || {};
+
+ var attrPath = Backbone.NestedModel.attrPath(attrStr),
+ aryPath = _.initial(attrPath),
+ val = this.get(aryPath, {silent: true}),
+ i = _.last(attrPath);
+
+ if (!_.isArray(val)){
+ throw new Error("remove() must be called on a nested array");
+ }
+
+ // only trigger if an element is actually being removed
+ var trigger = !opts.silent && (val.length > i + 1),
+ oldEl = val[i];
+
+ // remove the element from the array
+ val.splice(i, 1);
+ this.set(attrStr, val, opts);
+
+ if (trigger){
+ this.trigger('remove:' + Backbone.NestedModel.createAttrStr(aryPath), this, oldEl);
+ }
+
+ return this;
+ },
+
+ toJSON: function(){
+ var json = Backbone.NestedModel.__super__.toJSON.apply(this);
+ return Backbone.NestedModel.deepClone(json);
+ },
+
+
+ // private
+
+ _mergeAttrs: function(dest, source, opts, stack){
+ stack = stack || [];
+
+ _.each(source, function(sourceVal, prop){
+ if (prop === '-1'){
+ prop = dest.length;
+ }
+
+ var destVal = dest[prop],
+ newStack = stack.concat([prop]),
+ attrStr;
+
+ var isChildAry = _.isObject(sourceVal) && _.any(sourceVal, function(val, attr){
+ return attr === '-1' || _.isNumber(attr);
+ });
+
+ if (isChildAry && !_.isArray(destVal)){
+ destVal = dest[prop] = [];
+ }
+
+ if (prop in dest && _.isObject(sourceVal) && _.isObject(destVal)){
+ destVal = dest[prop] = this._mergeAttrs(destVal, sourceVal, opts, newStack);
+ } else {
+ var oldVal = destVal;
+
+ destVal = dest[prop] = sourceVal;
+
+ if (_.isArray(dest) && !opts.silent){
+ attrStr = Backbone.NestedModel.createAttrStr(stack);
+
+ if (!oldVal && destVal){
+ this.trigger('add:' + attrStr, this, destVal);
+ } else if (oldVal && !destVal){
+ this.trigger('remove:' + attrStr, this, oldVal);
+ }
+ }
+ }
+
+ // let the superclass handle change events for top-level attributes
+ if (!opts.silent && newStack.length > 1){
+ attrStr = Backbone.NestedModel.createAttrStr(newStack);
+ this.trigger('change:' + attrStr, this, destVal);
+ }
+ }, this);
+
+ return dest;
+ }
+
+ }, {
+ // class methods
+
+ attrPath: function(attrStrOrPath){
+ var path;
+
+ if (_.isString(attrStrOrPath)){
+ // change all appends to '-1'
+ attrStrOrPath = attrStrOrPath.replace(/\[\]/g, '[-1]');
+ // TODO this parsing can probably be more efficient
+ path = (attrStrOrPath === '') ? [''] : attrStrOrPath.match(/[^\.\[\]]+/g);
+ path = _.map(path, function(val){
+ // convert array accessors to numbers
+ return val.match(/^\d+$/) ? parseInt(val, 10) : val;
+ });
+ } else {
+ path = attrStrOrPath;
+ }
+
+ return path;
+ },
+
+ createAttrObj: function(attrStrOrPath, val){
+ var attrPath = this.attrPath(attrStrOrPath),
+ newVal;
+
+ switch (attrPath.length){
+ case 0:
+ throw "no valid attributes: '" + attrStrOrPath + "'";
+
+ case 1: // leaf
+ newVal = val;
+ break;
+
+ default: // nested attributes
+ var otherAttrs = _.rest(attrPath);
+ newVal = this.createAttrObj(otherAttrs, val);
+ break;
+ }
+
+ var childAttr = attrPath[0],
+ result = _.isNumber(childAttr) ? [] : {};
+
+ result[childAttr] = newVal;
+ return result;
+ },
+
+ createAttrStr: function(attrPath){
+ var attrStr = attrPath[0];
+ _.each(_.rest(attrPath), function(attr){
+ attrStr += _.isNumber(attr) ? ('[' + attr + ']') : ('.' + attr);
+ });
+
+ return attrStr;
+ },
+
+ deepClone: function(obj){
+ return $.extend(true, {}, obj);
+ }
+
+ });
+
+})();
--- /dev/null
+/**
+ * Backbone-Nested 1.1.0 - An extension of Backbone.js that keeps track of nested attributes
+ *
+ * http://afeld.github.com/backbone-nested/
+ *
+ * Copyright (c) 2011-2012 Aidan Feldman
+ * MIT Licensed (LICENSE)
+ *//*global Backbone, _, $ */(function(){"use strict",Backbone.NestedModel=Backbone.Model.extend({get:function(a,b){b=b||{};var c=Backbone.NestedModel.attrPath(a),d=c[0],e=Backbone.NestedModel.__super__.get.call(this,d);for(var f=1;f<c.length;f++){if(!e)break;d=c[f],e=e[d]}return!b.silent&&_.isObject(e)&&window.console&&window.console.log("Backbone-Nested syntax is preferred for accesing values of attribute '"+a+"'."),e},has:function(a){var b=this.get(a,{silent:!0});return b!==null&&!_.isUndefined(b)},set:function(a,b,c){var d;_.isObject(a)||a==null?(d=a,c=b):(d={},d[a]=b),c=c||{};var e=Backbone.NestedModel.deepClone(this.attributes),f,g,h;for(var i in d)g=Backbone.NestedModel.attrPath(i),h=Backbone.NestedModel.createAttrObj(g,d[i]),this._mergeAttrs(e,h,c);return Backbone.NestedModel.__super__.set.call(this,e,c)},unset:function(a,b){return b=_.extend({},b,{unset:!0}),this.set(a,null,b),this},add:function(a,b,c){var d=this.get(a,{silent:!0});this.set(a+"["+d.length+"]",b,c)},remove:function(a,b){b=b||{};var c=Backbone.NestedModel.attrPath(a),d=_.initial(c),e=this.get(d,{silent:!0}),f=_.last(c);if(!_.isArray(e))throw new Error("remove() must be called on a nested array");var g=!b.silent&&e.length>f+1,h=e[f];return e.splice(f,1),this.set(a,e,b),g&&this.trigger("remove:"+Backbone.NestedModel.createAttrStr(d),this,h),this},toJSON:function(){var a=Backbone.NestedModel.__super__.toJSON.apply(this);return Backbone.NestedModel.deepClone(a)},_mergeAttrs:function(a,b,c,d){return d=d||[],_.each(b,function(b,e){e==="-1"&&(e=a.length);var f=a[e],g=d.concat([e]),h,i=_.isObject(b)&&_.any(b,function(a,b){return b==="-1"||_.isNumber(b)});i&&!_.isArray(f)&&(f=a[e]=[]);if(e in a&&_.isObject(b)&&_.isObject(f))f=a[e]=this._mergeAttrs(f,b,c,g);else{var j=f;f=a[e]=b,_.isArray(a)&&!c.silent&&(h=Backbone.NestedModel.createAttrStr(d),!j&&f?this.trigger("add:"+h,this,f):j&&!f&&this.trigger("remove:"+h,this,j))}!c.silent&&g.length>1&&(h=Backbone.NestedModel.createAttrStr(g),this.trigger("change:"+h,this,f))},this),a}},{attrPath:function(a){var b;return _.isString(a)?(a=a.replace(/\[\]/g,"[-1]"),b=a===""?[""]:a.match(/[^\.\[\]]+/g),b=_.map(b,function(a){return a.match(/^\d+$/)?parseInt(a,10):a})):b=a,b},createAttrObj:function(a,b){var c=this.attrPath(a),d;switch(c.length){case 0:throw"no valid attributes: '"+a+"'";case 1:d=b;break;default:var e=_.rest(c);d=this.createAttrObj(e,b)}var f=c[0],g=_.isNumber(f)?[]:{};return g[f]=d,g},createAttrStr:function(a){var b=a[0];return _.each(_.rest(a),function(a){b+=_.isNumber(a)?"["+a+"]":"."+a}),b},deepClone:function(a){return $.extend(!0,{},a)}})})();
\ No newline at end of file