mirror of
https://github.com/moparisthebest/kaiwa
synced 2024-11-17 14:55:05 -05:00
566 lines
17 KiB
JavaScript
566 lines
17 KiB
JavaScript
// (c) 2013 Henrik Joreteg
|
|
// MIT Licensed
|
|
// For all details and documentation:
|
|
// https://github.com/HenrikJoreteg/StrictModel
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Initial setup
|
|
// -------------
|
|
|
|
// Establish the root object, `window` in the browser, or `global` on the server.
|
|
var root = this;
|
|
|
|
// The top-level namespace. All public Backbone classes and modules will
|
|
// be attached to this. Exported for both CommonJS and the browser.
|
|
var Strict = typeof exports !== 'undefined' ? exports : root.Strict = {},
|
|
toString = Object.prototype.toString,
|
|
slice = Array.prototype.slice;
|
|
|
|
// Current version of the library. Keep in sync with `package.json`.
|
|
Strict.VERSION = '0.0.1';
|
|
|
|
// Require Underscore, if we're on the server, and it's not already present.
|
|
var _ = root._;
|
|
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
|
|
|
|
// Require Backbone, if we're on the server, and it's not already present.
|
|
var Backbone = root.Backbone;
|
|
if (!Backbone && (typeof require !== 'undefined')) Backbone = require('backbone');
|
|
|
|
// Backbone Collection compatibility fix:
|
|
// In backbone, when you add an already instantiated model to a collection
|
|
// the collection checks to see if what you're adding is already a model
|
|
// the problem is, it does this witn an instanceof check. We're wanting to
|
|
// use completely different models so the instanceof will fail even if they
|
|
// are "real" models. So we work around this by overwriting this method from
|
|
// backbone 1.0.0. The only difference is it compares against our Strict.Model
|
|
// instead of backbone's.
|
|
Backbone.Collection.prototype._prepareModel = function (attrs, options) {
|
|
if (attrs instanceof Strict.Model) {
|
|
if (!attrs.collection) attrs.collection = this;
|
|
return attrs;
|
|
}
|
|
options || (options = {});
|
|
options.collection = this;
|
|
var model = new this.model(attrs, options);
|
|
if (!model._validate(attrs, options)) {
|
|
this.trigger('invalid', this, attrs, options);
|
|
return false;
|
|
}
|
|
return model;
|
|
};
|
|
|
|
// Helpers
|
|
// -------
|
|
|
|
// Shared empty constructor function to aid in prototype-chain creation.
|
|
var Constructor = function () {};
|
|
|
|
// Helper function to correctly set up the prototype chain, for subclasses.
|
|
// Similar to `goog.inherits`, but uses a hash of prototype properties and
|
|
// class properties to be extended.
|
|
var inherits = function (parent, protoProps, staticProps) {
|
|
var child;
|
|
|
|
// The constructor function for the new subclass is either defined by you
|
|
// (the "constructor" property in your `extend` definition), or defaulted
|
|
// by us to simply call the parent's constructor.
|
|
if (protoProps && protoProps.hasOwnProperty('constructor')) {
|
|
child = protoProps.constructor;
|
|
} else {
|
|
child = function () { return parent.apply(this, arguments); };
|
|
}
|
|
|
|
// Inherit class (static) properties from parent.
|
|
_.extend(child, parent);
|
|
|
|
// Set the prototype chain to inherit from `parent`, without calling
|
|
// `parent`'s constructor function.
|
|
Constructor.prototype = parent.prototype;
|
|
child.prototype = new Constructor();
|
|
|
|
// Add prototype properties (instance properties) to the subclass,
|
|
// if supplied.
|
|
if (protoProps) _.extend(child.prototype, protoProps);
|
|
|
|
// Add static properties to the constructor function, if supplied.
|
|
if (staticProps) _.extend(child, staticProps);
|
|
|
|
// Correctly set child's `prototype.constructor`.
|
|
child.prototype.constructor = child;
|
|
|
|
// Set a convenience property in case the parent's prototype is needed later.
|
|
child.__super__ = parent.prototype;
|
|
|
|
return child;
|
|
};
|
|
|
|
var extend = function (protoProps, classProps) {
|
|
var child = inherits(this, protoProps, classProps);
|
|
child.extend = this.extend;
|
|
return child;
|
|
};
|
|
|
|
// Mixins
|
|
// ------
|
|
|
|
// Sugar for defining properties a la ES5.
|
|
var Mixins = Strict.Mixins = {
|
|
// shortcut for Object.defineProperty
|
|
define: function (name, def) {
|
|
Object.defineProperty(this, name, def);
|
|
},
|
|
|
|
defineGetter: function (name, handler) {
|
|
this.define(name, {
|
|
get: handler.bind(this)
|
|
});
|
|
},
|
|
|
|
defineSetter: function (name, handler) {
|
|
this.define(name, {
|
|
set: handler.bind(this)
|
|
});
|
|
}
|
|
};
|
|
|
|
// Strict.Registry
|
|
// ---------------
|
|
|
|
// Internal storage for models, seperate namespace
|
|
// storage from default to prevent collision of matching
|
|
// model type+id and namespace name
|
|
|
|
var Registry = Strict.Registry = function () {
|
|
this._cache = {};
|
|
this._namespaces = {};
|
|
};
|
|
|
|
// Attach all inheritable methods to the Registry prototype.
|
|
_.extend(Registry.prototype, {
|
|
// Get the general or namespaced internal cache
|
|
_getCache: function (ns) {
|
|
if (ns) {
|
|
this._namespaces[ns] || (this._namespaces[ns] = {});
|
|
return this._namespaces[ns];
|
|
}
|
|
return this._cache;
|
|
},
|
|
|
|
// Find the cached model
|
|
lookup: function (type, id, ns) {
|
|
var cache = this._getCache(ns);
|
|
return cache && cache[type + id];
|
|
},
|
|
|
|
// Add a model to the cache if it has not already been set
|
|
store: function (model) {
|
|
var cache = this._getCache(model._namespace),
|
|
key = model.type + model.id;
|
|
// Prevent overriding a previously stored model
|
|
cache[key] = cache[key] || model;
|
|
return this;
|
|
},
|
|
|
|
// Remove a stored model from the cache, return `true` if removed
|
|
remove: function (type, id, ns) {
|
|
var cache = this._getCache(ns);
|
|
if (this.lookup.apply(this, arguments)) {
|
|
delete cache[type + id];
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// Reset internal cache
|
|
clear: function () {
|
|
this._cache = {};
|
|
this._namespaces = {};
|
|
}
|
|
});
|
|
|
|
// Create the default Strict.registry.
|
|
Strict.registry = new Registry();
|
|
|
|
// Strict.Model
|
|
// ------------
|
|
|
|
var Model = Strict.Model = function (attrs, options) {
|
|
attrs = attrs || {};
|
|
options = options || {};
|
|
|
|
var modelFound,
|
|
opts = _.defaults(options || {}, {
|
|
seal: true
|
|
});
|
|
|
|
this._namespace = opts.namespace;
|
|
this._initted = false;
|
|
this._deps = {};
|
|
this._initProperties();
|
|
this._initCollections();
|
|
this._cache = {};
|
|
this._verifyRequired();
|
|
this.set(attrs, {silent: true});
|
|
this.init.apply(this, arguments);
|
|
if (attrs.id) Strict.registry.store(this);
|
|
this._previous = _.clone(this.attributes); // Should this be set right away?
|
|
this._initted = true;
|
|
};
|
|
|
|
// Attach all inheritable methods to the Model prototype.
|
|
_.extend(Model.prototype, Backbone.Events, Mixins, {
|
|
idAttribute: 'id',
|
|
idDefinition: {
|
|
type: 'number',
|
|
setOnce: true
|
|
},
|
|
|
|
// stubbed out to be overwritten
|
|
init: function () {
|
|
return this;
|
|
},
|
|
|
|
// Remove model from the registry and unbind events
|
|
remove: function () {
|
|
if (this.id) {
|
|
Strict.registry.remove(this.type, this.id, this._namespace);
|
|
}
|
|
this.trigger('remove', this);
|
|
this.off();
|
|
return this;
|
|
},
|
|
|
|
set: function (key, value, options) {
|
|
var self = this,
|
|
changing = self._changing,
|
|
opts,
|
|
changes = [],
|
|
newType,
|
|
interpretedType,
|
|
newVal,
|
|
def,
|
|
attr,
|
|
attrs,
|
|
val;
|
|
|
|
self._changing = true;
|
|
|
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
|
if (_.isObject(key) || key === null) {
|
|
attrs = key;
|
|
options = value;
|
|
} else {
|
|
attrs = {};
|
|
attrs[key] = value;
|
|
}
|
|
|
|
opts = options || {};
|
|
|
|
// For each `set` attribute...
|
|
for (attr in attrs) {
|
|
val = attrs[attr];
|
|
newType = typeof val;
|
|
newVal = val;
|
|
|
|
def = this.definition[attr] || {};
|
|
|
|
// check type if we have one
|
|
if (def.type === 'date') {
|
|
if (!_.isDate(val)) {
|
|
try {
|
|
newVal = (new Date(parseInt(val, 10))).valueOf();
|
|
newType = 'date';
|
|
} catch (e) {
|
|
newType = typeof val;
|
|
}
|
|
} else {
|
|
newType = 'date';
|
|
newVal = val.valueOf();
|
|
}
|
|
} else if (def.type === 'array') {
|
|
newType = _.isArray(val) ? 'array' : typeof val;
|
|
} else if (def.type === 'object') {
|
|
// we have to have a way of supporting "missing" objects.
|
|
// Null is an object, but setting a value to undefined
|
|
// should work too, IMO. We just override it, in that case.
|
|
if (typeof val !== 'object' && _.isUndefined(val)) {
|
|
newVal = null;
|
|
newType = 'object';
|
|
}
|
|
}
|
|
|
|
// If we have a defined type and the new type doesn't match, throw error.
|
|
// Unless it's not required and the value is undefined.
|
|
if (def.type && def.type !== newType && (!def.required && !_.isUndefined(val))) {
|
|
throw new TypeError('Property \'' + attr + '\' must be of type ' + def.type + '. Tried to set ' + val);
|
|
}
|
|
|
|
// if trying to set id after it's already been set
|
|
// reject that
|
|
if (def.setOnce && def.value !== undefined && !_.isEqual(def.value, newVal)) {
|
|
throw new TypeError('Property \'' + key + '\' can only be set once.');
|
|
}
|
|
|
|
// only change if different
|
|
if (!_.isEqual(def.value, newVal)) {
|
|
self._previous && (self._previous[attr] = def.value);
|
|
def.value = newVal;
|
|
changes.push(attr);
|
|
}
|
|
}
|
|
|
|
_.each(changes, function (key) {
|
|
if (!opts.silent) {
|
|
self.trigger('change:' + key, self, self[key]);
|
|
}
|
|
// TODO: ensure that all deps are not undefined before triggering a change event
|
|
(self._deps[key] || []).forEach(function (derTrigger) {
|
|
// blow away our cache
|
|
delete self._cache[derTrigger];
|
|
if (!opts.silent) self.trigger('change:' + derTrigger, self, self.derived[derTrigger]);
|
|
});
|
|
});
|
|
|
|
// fire general change events
|
|
if (changes.length) {
|
|
if (!opts.silent) self.trigger('change', self);
|
|
}
|
|
},
|
|
|
|
get: function (attr) {
|
|
return this[attr];
|
|
},
|
|
|
|
// convenience methods for manipulating array properties
|
|
addListVal: function (prop, value, prepend) {
|
|
var list = _.clone(this[prop]) || [];
|
|
if (!_(list).contains(value)) {
|
|
list[prepend ? 'unshift' : 'push'](value);
|
|
this[prop] = list;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
previous: function (attr) {
|
|
return attr ? this._previous[attr] : _.clone(this._previous);
|
|
},
|
|
|
|
removeListVal: function (prop, value) {
|
|
var list = _.clone(this[prop]) || [];
|
|
if (_(list).contains(value)) {
|
|
this[prop] = _(list).without(value);
|
|
}
|
|
return this;
|
|
},
|
|
|
|
hasListVal: function (prop, value) {
|
|
return _.contains(this[prop] || [], value);
|
|
},
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
_initCollections: function () {
|
|
var coll;
|
|
if (!this.collections) return;
|
|
for (coll in this.collections) {
|
|
this[coll] = new this.collections[coll]();
|
|
this[coll].parent = this;
|
|
}
|
|
},
|
|
|
|
// Check that all required attributes are present
|
|
// TODO: should this throw an error or return boolean?
|
|
_verifyRequired: function () {
|
|
var attrs = this.attributes;
|
|
for (var def in this.definition) {
|
|
if (this.definition[def].required && typeof attrs[def] === 'undefined') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
_initProperties: function () {
|
|
var self = this,
|
|
definition = this.definition = {},
|
|
val,
|
|
prop,
|
|
item,
|
|
type,
|
|
filler;
|
|
|
|
this.cid = _.uniqueId('model');
|
|
|
|
function addToDef(name, val, isSession) {
|
|
var def = definition[name] = {};
|
|
if (_.isString(val)) {
|
|
// grab our type if all we've got is a string
|
|
type = self._ensureValidType(val);
|
|
if (type) def.type = type;
|
|
} else {
|
|
type = self._ensureValidType(val[0] || val.type);
|
|
if (type) def.type = type;
|
|
if (val[1] || val.required) def.required = true;
|
|
// set default if defined
|
|
def.value = !_.isUndefined(val[2]) ? val[2] : val.default;
|
|
if (isSession) def.session = true;
|
|
if (val.setOnce) def.setOnce = true;
|
|
}
|
|
}
|
|
|
|
// loop through given properties
|
|
for (item in this.props) {
|
|
addToDef(item, this.props[item]);
|
|
}
|
|
// loop through session props
|
|
for (prop in this.session) {
|
|
addToDef(prop, this.session[prop], true);
|
|
}
|
|
|
|
// always add "id" as a definition or make sure it's 'setOnce'
|
|
if (definition.id) {
|
|
definition[this.idAttribute].setOnce = true;
|
|
} else {
|
|
addToDef(this.idAttribute, this.idDefinition);
|
|
}
|
|
|
|
// register derived properties as part of the definition
|
|
this._registerDerived();
|
|
this._createGettersSetters();
|
|
|
|
// freeze attributes used to define object
|
|
if (this.session) Object.freeze(this.session);
|
|
//if (this.derived) Object.freeze(this.derived);
|
|
if (this.props) Object.freeze(this.props);
|
|
},
|
|
|
|
// just makes friendlier errors when trying to define a new model
|
|
// only used when setting up original property definitions
|
|
_ensureValidType: function (type) {
|
|
return _.contains(['string', 'number', 'boolean', 'array', 'object', 'date'], type) ? type : undefined;
|
|
},
|
|
|
|
_validate: function () {
|
|
return true;
|
|
},
|
|
|
|
_createGettersSetters: function () {
|
|
var item, def, desc, self = this;
|
|
|
|
// create getters/setters based on definitions
|
|
for (item in this.definition) {
|
|
def = this.definition[item];
|
|
desc = {};
|
|
// create our setter
|
|
desc.set = function (def, item) {
|
|
return function (val, options) {
|
|
self.set(item, val);
|
|
};
|
|
}(def, item);
|
|
// create our getter
|
|
desc.get = function (def, attributes) {
|
|
return function (val) {
|
|
if (typeof def.value !== 'undefined') {
|
|
if (def.type === 'date') {
|
|
return new Date(def.value);
|
|
}
|
|
return def.value;
|
|
}
|
|
return;
|
|
};
|
|
}(def);
|
|
|
|
// define our property
|
|
this.define(item, desc);
|
|
}
|
|
|
|
this.defineGetter('attributes', function () {
|
|
var res = {};
|
|
for (var item in this.definition) res[item] = this[item];
|
|
return res;
|
|
});
|
|
|
|
this.defineGetter('keys', function () {
|
|
return Object.keys(this.attributes);
|
|
});
|
|
|
|
this.defineGetter('json', function () {
|
|
return JSON.stringify(this._getAttributes(false, true));
|
|
});
|
|
|
|
this.defineGetter('derived', function () {
|
|
var res = {};
|
|
for (var item in this._derived) res[item] = this._derived[item].fn.apply(this);
|
|
return res;
|
|
});
|
|
|
|
this.defineGetter('toTemplate', function () {
|
|
return _.extend(this._getAttributes(true), this.derived);
|
|
});
|
|
},
|
|
|
|
_getAttributes: function (includeSession, raw) {
|
|
var res = {};
|
|
for (var item in this.definition) {
|
|
if (!includeSession) {
|
|
if (!this.definition[item].session) {
|
|
res[item] = (raw) ? this.definition[item].value : this[item];
|
|
}
|
|
} else {
|
|
res[item] = (raw) ? this.definition[item].value : this[item];
|
|
}
|
|
}
|
|
return res;
|
|
},
|
|
|
|
// stores an object of arrays that specifies the derivedProperties
|
|
// that depend on each attribute
|
|
_registerDerived: function () {
|
|
var self = this, depList;
|
|
if (!this.derived) return;
|
|
this._derived = this.derived;
|
|
for (var key in this.derived) {
|
|
depList = this.derived[key].deps || [];
|
|
_.each(depList, function (dep) {
|
|
self._deps[dep] = _(self._deps[dep] || []).union([key]);
|
|
});
|
|
|
|
// defined a top-level getter for derived keys
|
|
this.define(key, {
|
|
get: _.bind(function (key) {
|
|
// is this a derived property we should cache?
|
|
if (this._derived[key].cache) {
|
|
// do we have it?
|
|
if (this._cache.hasOwnProperty(key)) {
|
|
return this._cache[key];
|
|
} else {
|
|
return this._cache[key] = this._derived[key].fn.apply(this);
|
|
}
|
|
} else {
|
|
return this._derived[key].fn.apply(this);
|
|
}
|
|
}, this, key),
|
|
set: _.bind(function (key) {
|
|
var deps = this._derived[key].deps,
|
|
msg = '"' + key + '" is a derived property, you can\'t set it directly.';
|
|
if (deps && deps.length) {
|
|
throw new TypeError(msg + ' It is dependent on "' + deps.join('" and "') + '".');
|
|
} else {
|
|
throw new TypeError(msg);
|
|
}
|
|
}, this, key)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Set up inheritance for the model
|
|
Strict.Model.extend = extend;
|
|
|
|
// Overwrite Backbone.Model so that collections don't need to be modified in Backbone core
|
|
Backbone.Model = Strict.Model;
|
|
|
|
}).call(this);
|