/*jshint expr:true eqnull:true */ /** * * Backbone.DeepModel v0.10.4 * * Copyright (c) 2013 Charles Davison, Pow Media Ltd * * https://github.com/powmedia/backbone-deep-model * Licensed under the MIT License */ /** * Underscore mixins for deep objects * * Based on https://gist.github.com/echong/3861963 */ (function() { var arrays, basicObjects, deepClone, deepExtend, deepExtendCouple, isBasicObject, __slice = [].slice; deepClone = function(obj) { var func, isArr; if (!_.isObject(obj) || _.isFunction(obj)) { return obj; } if (obj instanceof Backbone.Collection || obj instanceof Backbone.Model) { return obj; } if (_.isDate(obj)) { return new Date(obj.getTime()); } if (_.isRegExp(obj)) { return new RegExp(obj.source, obj.toString().replace(/.*\//, "")); } isArr = _.isArray(obj || _.isArguments(obj)); func = function(memo, value, key) { if (isArr) { memo.push(deepClone(value)); } else { memo[key] = deepClone(value); } return memo; }; return _.reduce(obj, func, isArr ? [] : {}); }; isBasicObject = function(object) { if (object == null) return false; return (object.prototype === {}.prototype || object.prototype === Object.prototype) && _.isObject(object) && !_.isArray(object) && !_.isFunction(object) && !_.isDate(object) && !_.isRegExp(object) && !_.isArguments(object); }; basicObjects = function(object) { return _.filter(_.keys(object), function(key) { return isBasicObject(object[key]); }); }; arrays = function(object) { return _.filter(_.keys(object), function(key) { return _.isArray(object[key]); }); }; deepExtendCouple = function(destination, source, maxDepth) { var combine, recurse, sharedArrayKey, sharedArrayKeys, sharedObjectKey, sharedObjectKeys, _i, _j, _len, _len1; if (maxDepth == null) { maxDepth = 20; } if (maxDepth <= 0) { console.warn('_.deepExtend(): Maximum depth of recursion hit.'); return _.extend(destination, source); } sharedObjectKeys = _.intersection(basicObjects(destination), basicObjects(source)); recurse = function(key) { return source[key] = deepExtendCouple(destination[key], source[key], maxDepth - 1); }; for (_i = 0, _len = sharedObjectKeys.length; _i < _len; _i++) { sharedObjectKey = sharedObjectKeys[_i]; recurse(sharedObjectKey); } sharedArrayKeys = _.intersection(arrays(destination), arrays(source)); combine = function(key) { return source[key] = _.union(destination[key], source[key]); }; for (_j = 0, _len1 = sharedArrayKeys.length; _j < _len1; _j++) { sharedArrayKey = sharedArrayKeys[_j]; combine(sharedArrayKey); } return _.extend(destination, source); }; deepExtend = function() { var finalObj, maxDepth, objects, _i; objects = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), maxDepth = arguments[_i++]; if (!_.isNumber(maxDepth)) { objects.push(maxDepth); maxDepth = 20; } if (objects.length <= 1) { return objects[0]; } if (maxDepth <= 0) { return _.extend.apply(this, objects); } finalObj = objects.shift(); while (objects.length > 0) { finalObj = deepExtendCouple(finalObj, deepClone(objects.shift()), maxDepth); } return finalObj; }; _.mixin({ deepClone: deepClone, isBasicObject: isBasicObject, basicObjects: basicObjects, arrays: arrays, deepExtend: deepExtend }); }).call(this); /** * Main source */ ;(function(factory) { if (typeof define === 'function' && define.amd) { // AMD define(['underscore', 'backbone'], factory); } else { // globals factory(_, Backbone); } }(function(_, Backbone) { /** * Takes a nested object and returns a shallow object keyed with the path names * e.g. { "level1.level2": "value" } * * @param {Object} Nested object e.g. { level1: { level2: 'value' } } * @return {Object} Shallow object with path names e.g. { 'level1.level2': 'value' } */ function objToPaths(obj) { var ret = {}, separator = DeepModel.keyPathSeparator; for (var key in obj) { var val = obj[key]; if (val && val.constructor === Object && !_.isEmpty(val)) { //Recursion for embedded objects var obj2 = objToPaths(val); for (var key2 in obj2) { var val2 = obj2[key2]; ret[key + separator + key2] = val2; } } else { ret[key] = val; } } return ret; } /** * @param {Object} Object to fetch attribute from * @param {String} Object path e.g. 'user.name' * @return {Mixed} */ function getNested(obj, path, return_exists) { var separator = DeepModel.keyPathSeparator; var fields = path.split(separator); var result = obj; return_exists || (return_exists === false); for (var i = 0, n = fields.length; i < n; i++) { if (return_exists && !_.has(result, fields[i])) { return false; } result = result[fields[i]]; if (result == null && i < n - 1) { result = {}; } if (typeof result === 'undefined') { if (return_exists) { return true; } return result; } } if (return_exists) { return true; } return result; } /** * @param {Object} obj Object to fetch attribute from * @param {String} path Object path e.g. 'user.name' * @param {Object} [options] Options * @param {Boolean} [options.unset] Whether to delete the value * @param {Mixed} Value to set */ function setNested(obj, path, val, options) { options = options || {}; var separator = DeepModel.keyPathSeparator; var fields = path.split(separator); var result = obj; for (var i = 0, n = fields.length; i < n && result !== undefined ; i++) { var field = fields[i]; //If the last in the path, set the value if (i === n - 1) { options.unset ? delete result[field] : result[field] = val; } else { //Create the child object if it doesn't exist, or isn't an object if (typeof result[field] === 'undefined' || ! _.isObject(result[field])) { result[field] = {}; } //Move onto the next part of the path result = result[field]; } } } function deleteNested(obj, path) { setNested(obj, path, null, { unset: true }); } var DeepModel = Backbone.Model.extend({ // Override constructor // Support having nested defaults by using _.deepExtend instead of _.extend constructor: function(attributes, options) { var defaults; var attrs = attributes || {}; this.cid = _.uniqueId('c'); this.attributes = {}; if (options && options.collection) this.collection = options.collection; if (options && options.parse) attrs = this.parse(attrs, options) || {}; if (defaults = _.result(this, 'defaults')) { // // Replaced the call to _.defaults with _.deepExtend. attrs = _.deepExtend({}, defaults, attrs); // } this.set(attrs, options); this.changed = {}; this.initialize.apply(this, arguments); }, // Return a copy of the model's `attributes` object. toJSON: function(options) { return _.deepClone(this.attributes); }, // Override get // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name' get: function(attr) { return getNested(this.attributes, attr); }, // Override set // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name' set: function(key, val, options) { var attr, attrs, unset, changes, silent, changing, prev, current; if (key == null) return this; // Handle both `"key", value` and `{key: value}` -style arguments. if (typeof key === 'object') { attrs = key; options = val || {}; } else { (attrs = {})[key] = val; } options || (options = {}); // Run validation. if (!this._validate(attrs, options)) return false; // Extract attributes and options. unset = options.unset; silent = options.silent; changes = []; changing = this._changing; this._changing = true; if (!changing) { this._previousAttributes = _.deepClone(this.attributes); //: Replaced _.clone with _.deepClone this.changed = {}; } current = this.attributes, prev = this._previousAttributes; // Check for changes of `id`. if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; // attrs = objToPaths(attrs); // // For each `set` attribute, update or delete the current value. for (attr in attrs) { val = attrs[attr]; //: Using getNested, setNested and deleteNested if (!_.isEqual(getNested(current, attr), val)) changes.push(attr); if (!_.isEqual(getNested(prev, attr), val)) { setNested(this.changed, attr, val); } else { deleteNested(this.changed, attr); } unset ? deleteNested(current, attr) : setNested(current, attr, val); // } // Trigger all relevant attribute changes. if (!silent) { if (changes.length) this._pending = true; // var separator = DeepModel.keyPathSeparator; for (var i = 0, l = changes.length; i < l; i++) { var key = changes[i]; this.trigger('change:' + key, this, getNested(current, key), options); var fields = key.split(separator); //Trigger change events for parent keys with wildcard (*) notation for(var n = fields.length - 1; n > 0; n--) { var parentKey = _.first(fields, n).join(separator), wildcardKey = parentKey + separator + '*'; this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options); } // } } if (changing) return this; if (!silent) { while (this._pending) { this._pending = false; this.trigger('change', this, options); } } this._pending = false; this._changing = false; return this; }, // Clear all attributes on the model, firing `"change"` unless you choose // to silence it. clear: function(options) { var attrs = {}; var shallowAttributes = objToPaths(this.attributes); for (var key in shallowAttributes) attrs[key] = void 0; return this.set(attrs, _.extend({}, options, {unset: true})); }, // Determine if the model has changed since the last `"change"` event. // If you specify an attribute name, determine if that attribute has changed. hasChanged: function(attr) { if (attr == null) return !_.isEmpty(this.changed); return getNested(this.changed, attr) !== undefined; }, // Return an object containing all the attributes that have changed, or // false if there are no changed attributes. Useful for determining what // parts of a view need to be updated and/or what attributes need to be // persisted to the server. Unset attributes will be set to undefined. // You can also pass an attributes object to diff against the model, // determining if there *would be* a change. changedAttributes: function(diff) { //: objToPaths if (!diff) return this.hasChanged() ? objToPaths(this.changed) : false; // var old = this._changing ? this._previousAttributes : this.attributes; // diff = objToPaths(diff); old = objToPaths(old); // var val, changed = false; for (var attr in diff) { if (_.isEqual(old[attr], (val = diff[attr]))) continue; (changed || (changed = {}))[attr] = val; } return changed; }, // Get the previous value of an attribute, recorded at the time the last // `"change"` event was fired. previous: function(attr) { if (attr == null || !this._previousAttributes) return null; // return getNested(this._previousAttributes, attr); // }, // Get all of the attributes of the model at the time of the previous // `"change"` event. previousAttributes: function() { // return _.deepClone(this._previousAttributes); // } }); //Config; override in your app to customise DeepModel.keyPathSeparator = '.'; //Exports Backbone.DeepModel = DeepModel; //For use in NodeJS if (typeof module != 'undefined') module.exports = DeepModel; return Backbone; }));