//CoccyxJS 0.6.4 //(c) 2013 Jeffrey Schwartz //CoccyxJS may be freely distributed under the MIT license. //For all details and documentation: //http://coccyxjs.jitsu.com (function(){'use strict'; if(!(typeof define === 'function' && define.amd)) {window.define = function define(){(arguments[arguments.length - 1])();};} }()); define('helpers', [], function(){ 'use strict'; var v = window.Coccyx = window.Coccyx || {}; v.helpers = { //0.6.3 Returns true if options hash passed as last argument and option.silent is true, false otherwise. isSilent: function isSilent(args){return args.length && !Array.isArray(args[args.length - 1]) && typeof(args[args.length -1]) === 'object' && args[args.length - 1].silent;}, //Returns true if s1 contains s2, otherwise returns false. contains: function contains(s1, s2){if(typeof s1 === 'string'){for(var i = 0, len = s1.length; i < len; i++){if(s1[i] === s2) {return true;}}} return false;}, //Returns a deep copy object o. deepCopy: function deepCopy(o){return JSON.parse(JSON.stringify(o));}, //Pass one or more objects as the source objects whose properties are to be copied to the target object. extend: function extend(targetObj){ var property; for(var i = 1, len = arguments.length - 1; i <= len; i++){for(property in arguments[i]){if(arguments[i].hasOwnProperty(property)){targetObj[property] = arguments[i][property];}} } return targetObj; }, //For each matching property name, replaces target's value with source's value. replace: function replace(target, source){ for(var prop in target){if(target.hasOwnProperty(prop) && source.hasOwnProperty(prop)){target[prop] = source[prop];}} //0.6.0 Return target. return target; } }; }); define('application', ['jquery'], function(){ 'use strict'; var v = window.Coccyx = window.Coccyx || {}, controllers = {}, routes = {}, VERSION = '0.6.4'; function registerControllers(){ if(arguments.length !== 1 && !(arguments[0] instanceof Array) && !(arguments[0] instanceof Object)){ console.log('registerControllers missing or invalid param. Expected an [] or {}.'); } if(arguments[0] instanceof Array){ //An array of hashes. arguments[0].forEach(function(controller){loadRoutesFromController(controller);}); }else{ //A single hash. loadRoutesFromController(arguments[0]); } } function loadRoutesFromController(controller){ var namedRoute; console.log('Registering controller \'' + controller.name + '\''); //controller's local $ controller.$ = v.$; //Maintain list of controllers for when we need to bind them to route function callbacks. controllers[controller.name] = controller; //Build the routes array. for(var route in controller.routes){ if(controller.routes.hasOwnProperty(route)){ //Verb + ' /'. namedRoute = route.substring(0, route.indexOf(' ') + 1) + '/'; //Controller name (the root segment). namedRoute += controller.name; //Remaining path. namedRoute += (route.substring(route.indexOf(' ') + 1) === '/' ? '' : controller.name === '' ? route.substring(route.indexOf(' ') + 1) : '/' + route.substring(route.indexOf(' ') + 1)); routes[namedRoute] = [controller.name,controller.routes[route]]; console.log('Registering route \'' + namedRoute + '\''); } } } function getRoutes(){return routes;} function getController(name){return controllers[name];} //Provide jQuery in the Coccyx name space. v.$ = jQuery; //0.6.0 Renamed userspace to application - provides a bucket for application stuff. v.application = v.application || {}; //0.6.1 init will be called only once immediately before the first routing request //is handled by the router. Override init to provide application specific initialization, //such as bootstrapping your application with data. v.init = function init(){}; //Provide a bucket for Coccyx library plug-ins. v.plugins = v.plugins || {}; //Version stamp. v.getVersion = function(){return VERSION;}; v.controllers = {registerControllers : registerControllers, getRoutes: getRoutes, getController: getController}; }); define('ajax', ['helpers', 'application'], function(){ 'use strict'; //v0.6.4 added 'json' as default dataType. var v = window.Coccyx = window.Coccyx || {}, defaultSettings = {dataType: 'json', cache: false, url: '/'}, extend = v.helpers.extend, ajax = v.$.ajax; //Merge default setting with user's settings. function mergeSettings(settings, type){settings.type = type; return extend({}, defaultSettings, settings);} //A simple promise-based wrapper around jQuery Ajax. All methods return a Promise. v.ajax = { //http "GET" ajaxGet: function ajaxGet(settings){return ajax(mergeSettings(settings, 'GET'));}, //http "POST" ajaxPost: function ajaxPost(settings){return ajax(mergeSettings(settings, 'POST'));}, //http "PUT" ajaxPut: function ajaxPut(settings){return ajax(mergeSettings(settings, 'PUT'));}, //http "DELETE" ajaxDelete: function ajaxDelete(settings){return ajax(mergeSettings(settings, 'DELETE'));} }; }); //0.6.0 define('eventer', ['helpers', 'application'], function(){ 'use strict'; //Coccyx.eventer turns any object into an "eventing" object and is based on jQuery's eventing model. //Example: v.eventer.extend(someObjet); someObject.handle('ping', callback); ... someObject.trigger('ping'); var v = window.Coccyx = window.Coccyx || {}, eventerApi; eventerApi = { //Attach a callback handler to a specific custom event or events fired from 'this' object optionally binding the callback to context. handle: function handle(events, callback, context){v.$(this._eventedObj).on(events, context? v.$.proxy(callback, context) : callback);}, //Like handle but will only fire the event one time and will ignore subsequent events. handleOnce: function handleOnce(events, callback, context){v.$(this._eventedObj).one(events, context? v.$.proxy(callback, context) : callback);}, //Removes event handler. off: function off(events, callback){ if(events && callback){ v.$(this._eventedObj).off(events, callback); }else if(events){ //0.6.3 removes all event handlers for events v.$(this._eventedObj).off(events); }else{ //0.6.3 removes all event handlers for all events v.$(this._eventedObj).off(); } }, //Trigger an event for object optionally passing args if provided. trigger: function trigger(events, args){v.$(this._eventedObj).trigger(events, args);} }; function extend(obj){ //0.6.3 Implementation now placed directly on obj instead of added as a prototype. var obj0 = obj._eventedObj ? obj : v.helpers.extend(obj, eventerApi); //0.6.3 Add eventedObj to obj. obj0._eventedObj = obj._eventedObj ? obj._eventedObj : {}; return obj0; } v.eventer = {extend: extend}; }); define('router', ['helpers', 'application'], function() { 'use strict'; var v = window.Coccyx = window.Coccyx || {}, contains = v.helpers.contains; function route(verb, url, valuesHash){ //0.6.1 Call Coccyx.init() only once before handling any routing requests. See application.js for details. if(!v.initCalled){ v.init(); v.initCalled = true; console.log('Coccyx.init called'); } var rt = getRoute(verb, url); if(rt){routeFound(rt, valuesHash);} else{routeNotFound(verb, url);} } function getRoute(verb, url){ var routes = v.controllers.getRoutes(), a = url.substring(1).split('/'), params = [], rel = false, route, b, c, eq, vrb; for(route in routes){ if(routes.hasOwnProperty(route)){ //Get the 'veb'. vrb = route.substring(0, route.indexOf(' ')); //Get the url. b = route.substring(route.indexOf('/') + 1).split('/'); if(verb === vrb && (a.length === b.length || contains(route, '*'))){ eq = true; //The url and the route have the same number of segments so the route can be either static or it could contain parameterized segments. for(var i = 0, len = b.length; i < len; i++){ //If the segments are equal then continue looping. if(a[i] === b[i]){continue;} //If the route segment is parameterized then save the parameter and continue looping. if(contains(b[i],':')){ //0.4.0 - checking for 'some:thing' c = b[i].split(':'); if(c.length === 2){if(a[i].substr(0, c[0].length) === c[0]){params.push(a[i].substr(c[0].length));} } else{params.push(a[i]);} continue; } //If the route is a relative route, push it onto the array and break out of the loop. if(contains(b[i], '*')){rel = true; eq = false; break;} //If none of the above eq = false; break; } //The route matches the url so attach the params (it could be empty) to the route and return the route. if(eq){ //controller name, function to call, function arguments to call with... return {controllerName: routes[route][0], fn: routes[route][1], params: params}; } if(rel){ //controller name, function to call, function arguments to call with... for(var ii = i, llen = a.length, relUrl = ''; ii < llen; ii++){relUrl += ('/' + a[ii]);} //controller name, function to call, function arguments to call with... return {controllerName: routes[route][0], fn: routes[route][1], params: [relUrl]}; } } } } } function routeFound(route, valuesHash){ //0.6.0 Prior versions called controller.init() when the controller is loaded. Starting with 0.6.0, //controller.init() is only called when routing is called to one of their route callbacks. This //eliminates unnecessary initialization if the controller is never used. var controller = v.controllers.getController(route.controllerName); if(controller.hasOwnProperty('init') && !controller.hasOwnProperty('initCalled')){ controller.init(); controller.initCalled = true; } //Route callbacks are bound (their contexts (their 'this')) to their controllers. if(valuesHash){route.fn.call(controller, valuesHash); } else if(route.params.length){route.fn.apply(controller, route.params); } else{route.fn.call(controller);} } function routeNotFound(url){console.log('router::routeNotFound called with route = ' + url);} v.router = {route: route}; }); define('history', ['application', 'router'], function() { 'use strict'; //Verify browser supports pushstate. console.log(history.pushState ? 'history pushState is supported in your browser' : 'history pushstate is not supported in your browser'); var v = window.Coccyx = window.Coccyx || {}, historyStarted = false; //Event handler for click event on anchor tags. Ignores those where the href path doesn't start with //a '/' character; this prevents handling external links, allowing those events to bubble up as normal. v.$(document).on('click', 'a', function(event){ if(v.$(this).attr('href').indexOf('/') === 0){ event.preventDefault(); //0.6.0 changed target to currentTarget. var pathName = event.currentTarget.pathname; //The 'verb' for routes on anchors is always 'get'. v.router.route('get', pathName); //0.6.0 changed target to currentTarget. history.pushState({verb: 'get'}, null, event.currentTarget.href); } }); //Event handler for form submit event. Ignores submit events on forms whose action attributes do not //start with a '/' character; this prevents handling form submit events for forms whose action //attribute values are external links, allowing those events to bubble up as normal. v.$(document).on('submit', 'form', function(event){ var $form = v.$(this), action = $form.attr('action'), method = $form.attr('method'), valuesHash; if(action.indexOf('/') === 0){ event.preventDefault(); method = method ? method : 'get'; valuesHash = valuesHashFromSerializedArray($form.serializeArray()); v.router.route(method, action, valuesHash); } }); //Event handler for popstate event. v.$(window).on('popstate', function(event){ //Ignore 'popstate' events without state and until history.start is called. if(event.originalEvent.state && started()){v.router.route(event.originalEvent.state.verb , window.location.pathname);} }); //Creates a hash from an array whose elements are hashes whose properties are 'name' and 'value'. function valuesHashFromSerializedArray(valuesArray){ var valuesHash = {}; for(var i = 0, len = valuesArray.length; i < len; i++){valuesHash[valuesArray[i].name] = valuesArray[i].value;} return valuesHash; } function started(){return historyStarted;} //Call Coccyx.history.start to start your application. When called starts responding to //'popstate' events which are raised when the user uses the browser's back and forward //buttons to navigate. Pass true for trigger if you want the route function to be called. //0.5.0 function start(trigger, controllers){ v.controllers.registerControllers(controllers); //0.5.0 historyStarted = true; history.replaceState({verb: 'get'}, null, window.location.pathname); if(trigger){v.router.route('get', window.location.pathname);} } //A wrapper for the browser's history.pushState and history.replaceState. Whenever you reach //a point in your application that you'd like to save as a URL, call navigate in order to update //the URL. If you wish to also call the route function, set the trigger option to true. To update //the URL without creating an entry in the browser's history, set the replace option to true. //Pass true for trigger if you want the route function to be called. //Pass true for replace if you only want to replace the current history entry and not //push a new one onto the browser's history stack. //function navigate(state, title, url, trigger, replace){ function navigate(options){ if(v.history.started()){ options = options || {}; options.state = options.state || null; options.title = options.title || document.title; options.method = options.method || 'get'; options.url = options.url || window.location.pathname; options.trigger = options.trigger || false; options.replace = options.replace || false; window.history[options.replace ? 'replaceState' : 'pushState'](options.state, options.title, options.url); if(options.trigger){v.router.route(options.method, options.url);} } } v.history = {start: start, started: started, navigate: navigate}; }); define('models', ['helpers', 'ajax', 'eventer', 'application'], function(){ 'use strict'; /** * Warning!!!! Don't use primitive object wrappers, Date objects or functions as data property values. * This is because model uses JSON.parse(JSON.stringify(data)) to perform a deep copy of your model * data and JSON doesn't support primitive object wrappers, Date objects or functions. * (see https://developer.mozilla.org/en-US/docs/JSON for details) * Deep copying is necessary to isolate the originalValues and changedValues from * changes made in data. If you don't heed this warning bad things _will_ happen!!! */ var v = window.Coccyx = window.Coccyx || {}, ajax = v.ajax, deepCopy = v.helpers.deepCopy, ext = v.helpers.extend, replace = v.helpers.replace, proto, propertyChangedEvent = 'MODEL_PROPERTY_CHANGED_EVENT', syntheticId = -1, isSilent = v.helpers.isSilent; //0.6.0 Publishes MODEL_PROPERTY_CHAGED_EVENT event via Coccyx.eventer. function publishPropertyChangeEvent(model, propertyPath, value){model.trigger(propertyChangedEvent, {propertyPath: propertyPath, value: value, model: model});} //0.6.0 Return the property reachable through the property path or undefined. function findProperty(obj, propertyPath){ //0.6.0 Return false if obj is an array or not an object. if(Array.isArray(obj) || typeof obj !== 'object'){return;} var a = propertyPath.split('.'); if(a.length === 1){return obj[propertyPath];} //Try the next one in the chain. return findProperty(obj[a[0]], a.slice(1).join('.')); } //0.6.0 Sets the property reachable through the property path, creating it first if necessary, with a deep copy of val. function findAndSetProperty(obj, propertyPath, val){ var a = propertyPath.split('.'); if(a.length === 1){obj[propertyPath] = typeof val === 'object' ? deepCopy(val) : val;} else{ if(!obj.hasOwnProperty(a[0])){obj[a[0]] = {};} findAndSetProperty(obj[a[0]], a.slice(1).join('.'), val); } } //0.6.0 Deletes the property reachable through the property path. function findAndDeleteProperty(obj, propertyPath){ var a = propertyPath.split('.'); if(a.length === 1){delete obj[propertyPath];} else{ if(!obj.hasOwnProperty(a[0])){obj[a[0]] = {};} findAndDeleteProperty(obj[a[0]], a.slice(1).join('.')); } } //0.5.0 //0.6.0 Added support for Coccyx.eventer. function extend(modelObject){ var obj0 = Object.create(proto), obj1 = modelObject ? ext(obj0, modelObject) : obj0, obj2 = modelObject ? Object.create(obj1) : obj1; //Decorate the new object with additional properties. obj2.isSet = false; obj2.isReadOnly = false; obj2.isDirty = false; obj2.originalData = {}; obj2.changedData = {}; obj2.data = {}; //0.6.3 Eventer no longer placed on prototype as in prior versions. Its methods are now mixed in with the final object. return v.eventer.extend(obj2); } //0.6.0. function setAjaxSettings(settings, verb){ /*jshint validthis:true*/ settings = settings ? settings : {}; //0.6.4. settings.url = this.endPoint; if(verb !== 'post'){settings.url += ('/' + this.data[this.idPropertyName]);} if(verb !== 'get'){settings.data = this.getData();} return settings; } //0.6.0 Returns a promise. 0.6.4 Renamed to doAjax, added settings argument, removed rawJson option & added call to model.parse(). function doAjax(verb, settings, fn){ /*jshint validthis:true*/ var deferred = v.$.Deferred(), self = this, promise; promise = fn(setAjaxSettings.call(this, settings, verb)); promise.done(function(data){ //0.6.4. If 'get' and data was returned call parse. data = verb === 'get' && data ? self.parse(data) : data; //If data was returned set this model's data. if(data){self.setData(ext(self.getData(),data));} //Calls promise.done passing this model. deferred.resolve(self); }); //Calls promise.fail. 0.6.4 now returns this model as 1st argument. promise.fail(function(jqXHR, textStatus, errorThrown){deferred.reject(self, jqXHR, textStatus, errorThrown);}); return deferred.promise(); } //model prototype properties... proto = { setData: function setData (dataHash, options) { var o = {empty:false, readOnly:false, dirty:false, validate: false}; //Merge default options with passed in options. if(options){replace(o, options);} //If options validate is true and there is a validate method and it returns false, sets valid to false and returns false. //If options validate is true and there is a validate method and it returns true, sets valid to true and proceeds with setting data. //If options validate is false or there isn't a validate method set valid to true. this.isValid = o.validate && this.validate ? this.validate(dataHash) : true; if(!this.isValid){return false;} //Deep copy. this.originalData = o.empty ? {} : deepCopy(dataHash); this.isReadOnly = o.readOnly; this.isDirty = o.dirty; //Deep copy. this.data = deepCopy(dataHash); //0.6.0 Every model has an idPropetyName whose value is the name of the model's data id property. if(typeof this.idPropertyName === 'undefined') {this.idPropertyName = 'id';} //0.6.0 Every model has a modelId property, either a synthetic one (see syntheticId, above) //or one provided by the model's data and whose property name is this.idPropertyName. this.modelId = this.data.hasOwnProperty(this.idPropertyName) ? this.data[this.idPropertyName] : syntheticId--; this.changedData = {}; this.isSet = true; return true; }, getData: function getData(){return deepCopy(this.data);}, //Returns deep copy of originalData getOriginalData: function getOriginalData(){return deepCopy(this.originalData);}, //Returns deep copy of changedData getChangedData: function getChangedData(){return deepCopy(this.changedData);}, //Returns the data property reachable through property path. If there is no property reachable through property path //return undefined. getProperty: function getProperty(propertyPath){var p = findProperty(this.data, propertyPath); return typeof p === 'object' ? deepCopy(p) : p;}, //Returns the originalData property reachable through property path. If there is no property reachable through property path //return undefined. getOriginalDataProperty: function getOriginalDataProperty(propertyPath){var p = findProperty(this.originalData, propertyPath); return typeof p === 'object' ? deepCopy(p) : p;}, //Returns the changedData property reachable through property path. If there is no property reachable through property path //return undefined. getChangedDataProperty: function getChangedDataProperty(propertyPath){var p = findProperty(this.changedData, propertyPath); return typeof p === 'object' ? deepCopy(p) : p;}, //Sets a property on an object reachable through the property path and fires publishPropertyChangeEvent if options.silent isn't //passed or is false. Maintains deletedColl. If the property doesn't exits, it will be created and then assigned its value //(using a deep copy if typeof data === 'object'). Calling set with a nested object or property is therefore supported. For //example, if the property path is address.street and the model's data hash is {name: 'some name'}, the result will be //{name: 'some name', address: {street: 'some street'}}; and the changed data hash will be {'address.street': some.street'}. setProperty: function setProperty(propertyPath, val){ //A model's data properties cannot be written to if the model hasn't been set yet or if the model is read only. if(this.isSet && !this.isReadOnly){ findAndSetProperty(this.data, propertyPath, val); this.changedData[propertyPath] = deepCopy(val); this.isDirty = true; //0.6.0 Maintain id's state when setting properties on data. if(this.data.hasOwnProperty(this.idPropertyName)){this.modelId = this.data[this.idPropertyName];} //0.6.3 Check for silent. if(!isSilent(arguments)){publishPropertyChangeEvent(this, propertyPath, val);} } else{console.log(!this.isSet ? 'Warning! setProperty called on model before set was called.' : 'Warning! setProperty called on read only model.');} //For chaining. return this; }, //0.6.0 Delete a property from this.data via property path and fires publishPropertyChangeEvent //if options.silent isn't passed or is false. Maintains deletedColl deleteProperty: function deleteProperty(propertyPath){ //A model's data properties cannot be deleted if the model hasn't been set yet or if the model is read only. if(this.isSet && !this.isReadOnly){ findAndDeleteProperty(this.data, propertyPath); this.changedData[propertyPath] = undefined; this.isDirty = true; if(this.data.hasOwnProperty(this.idPropertyName)){this.modelId = this.data[this.idPropertyName];} //0.6.3 Check for silent. if(!isSilent(arguments)){publishPropertyChangeEvent(this, propertyPath, undefined);} }else{console.log(!this.isSet ? 'Warning! deleteProperty called on model before set was called.' : 'Warning! deleteProperty called on read only model.');} }, //0.6.0 Returns true if model is new, false otherwise. isNew: function isNew(){return (typeof this.data[this.idPropertyName] === 'undefined');}, //0.6.0 Returns stringified model's data hash. toJSON: function toJSON(){return JSON.stringify(this.data);}, //0.6.4 For the 4 following ajax methods, ajaxSettings is a hash which can contain any valid jQuery.ajax setting. //0.6.0 Ajax "GET". ajaxGet: function ajaxGet(ajaxSettings){return doAjax.call(this, 'get', ajaxSettings, ajax.ajaxGet);}, //0.6.0 Ajax "POST". ajaxPost: function ajaxPost(ajaxSettings){return doAjax.call(this, 'post', ajaxSettings, ajax.ajaxPost);}, //0.6.0 Ajax "PUT". ajaxPut: function ajaxPut(ajaxSettings){return doAjax.call(this, 'put', ajaxSettings, ajax.ajaxPut);}, //0.6.0 Ajax "DELETE". ajaxDelete: function ajaxDelete(ajaxSettings){return doAjax.call(this, 'delete', ajaxSettings, ajax.ajaxDelete);}, //0.6.4 Override to transform the data returned from Ajax call and return json. parse: function parse(data){return data;} }; v.models = {extend: extend, propertyChangedEvent: propertyChangedEvent}; }); //0.6.0 define('collections', ['helpers', 'ajax', 'application', 'models'], function(){ 'use strict'; //0.6.3 added isSilent var v = window.Coccyx = window.Coccyx || {}, ext = v.helpers.extend, addEvent ='COLLECTION_MODEL_ADDED_EVENT', removeEvent ='COLLECTION_MODEL_REMOVED_EVENT', sortEvent ='COLLECTION_SORTED_EVENT', isSilent = v.helpers.isSilent, proto; function extend(collObj){ var obj0 = Object.create(proto), obj1 = collObj ? ext(obj0, collObj) : obj0; //Collections have to know what their models' id property names are. Defaults to 'id', unless provided. obj1.modelsIdPropertyName = obj1.model && typeof obj1.model.idPropertyName !== 'undefined' ? obj1.model.idPropertyName : typeof obj1.modelsIdPropertyName !== 'undefined' ? obj1.modelsIdPropertyName : 'id'; //Collections have to know what their models' endPoints are. Defaults to '/', unless provided. obj1.modelsEndPoint = obj1.model && typeof obj1.model.endPoint !== 'undefined' ? obj1.model.endPoint : typeof obj1.modelsEndPoint !== 'undefined' ? obj1.modelsEndPoint : '/'; var obj2 = Object.create(obj1); obj2.isReadOnly = false; obj2.coll = []; obj2.deletedColl = []; //0.6.3 Eventer no longer placed on prototype as in prior versions. Its methods are now mixed in with the final object. return v.eventer.extend(obj2); } //Returns an array containing the raw data for each model in the models array. function toRaw(models){if(Array.isArray(models)){return models.map(function(model){return model.getData();});} } function compareArrays(a, b){ if(Array.isArray(a) && Array.isArray(b)){ if(a.length !== b.length){return false;} for(var i = 0, len = a.length; i < len; i++){ if(typeof a[i] === 'object' && typeof b[i] === 'object'){if(!compareObjects(a[i], b[i])){return false;} continue;} if(typeof a[i] === 'object' || typeof b[i] === 'object'){return false;} if(Array.isArray(a[i]) && Array.isArray(b[i])){if(!compareArrays(a[i], b[i])){return false;} continue;} if(Array.isArray(a[i]) || Array.isArray(b[i])){return false;} if(a[i] !== b[i]){return false;} } return true; } return false; } function compareObjects(a, b){ var prop; if(compareArrays(a, b)){return true;} for(prop in a){ if(a.hasOwnProperty(prop) && b.hasOwnProperty(prop)){ if(typeof a[prop] === 'object' && typeof b[prop] === 'object'){if(!compareObjects(a[prop], b[prop])){return false;} continue;} if(typeof a[prop] === 'object' || typeof b[prop] === 'object'){return false;} if(a[prop] !== b[prop]){return false;} }else {return false;} } return true; } function compare(a, b){return compareObjects(a, b) && compareObjects(b, a);} //Returns true if element has the same properties as source and their values are equal, false otherwise. function isMatch(element, source){ for(var prop in source){ if(source.hasOwnProperty(prop) && element.hasOwnProperty(prop)){ if(typeof element[prop] === 'object' && typeof source[prop] === 'object'){if(!compare(element[prop], source[prop])){return false;}} else if(typeof element[prop] === 'object' || typeof source[prop] === 'object'){return false;} else{if(source[prop] !== element[prop]){return false;}} }else{return false;} } return true; } function isArrayOrNotObject(value){return Array.isArray(value) || typeof value !== 'object' ? true : false;} //If it walks and talks like a duck... Checks for the following properties on a model's data: //isSet, isReadOnly, isDirty, originalData, changedData, data. //If data has all of the above then 'it is' a model and returns true, otherwise it returns false. function isAModel(obj){ var markers = ['isSet', 'isReadOnly', 'isDirty', 'originalData', 'changedData', 'data']; for(var i = 0, len = markers.length; i < len; i++){if(!obj.hasOwnProperty(markers[i])){return false;}} return true; } //Makes a model from raw data and returns that model. function makeModelFromRaw(collObject, raw){ var model = v.models.extend(collObject.model ? collObject.model : {idPropertyName: collObject.modelsIdPropertyName, endPoint: collObject.modelsEndPoint}); model.setData(raw); return model; } //A simple general use, recursive iterator. Makes no assumptions about what args is. Args could be //anything - a function's arguments, an Array, an object or even a primitive. function iterate(args, callback){ //If args is an Array or it has a length property it is iterable. if(Array.isArray(args) || args.hasOwnProperty('length')){for(var i = 0, len = args.length; i < len; i++){iterate(args[i], callback);} } else{ //Not iterable. callback(args); } } //Wire the model's property change event to be handled by this collection. function wireModelPropertyChangedHandler(collObject, model){ model.handle(v.models.propertyChangedEvent, collObject.modelPropertyChangedHandler, collObject); return model; } //Pushes [models] onto the collection. [models] can be either models or raw data. If [models] is raw data, the raw data //will be turned into models first before being pushed into the collection. Collections proxy model property change events. function addModels(collObject, models){ var pushed = []; if(Array.isArray(models)){ models.forEach(function(model){ collObject.coll.push(wireModelPropertyChangedHandler(collObject, isAModel(model) ? model : makeModelFromRaw(collObject, model))); pushed.push(collObject.at(collObject.coll.length - 1)); }); }else{ collObject.coll.push(wireModelPropertyChangedHandler(collObject, isAModel(models) ? models : makeModelFromRaw(collObject, models))); pushed.push(collObject.at(collObject.coll.length - 1)); } return pushed; } //Calls iterate on args to generate and array of models. function argsToModels(collObject, args){ var models = []; iterate(args, function(arg){ var m = isAModel(arg) ? arg : makeModelFromRaw(collObject, arg); models.push(wireModelPropertyChangedHandler(collObject, m)); }); return models; } //0.6.3 Removes propertyChangedEvents from models. function removePropertyChangedEvents(models){ /*jshint validthis:true*/ var self = this; if(models){ if(Array.isArray(models)){models.forEach(function(m){m.off(v.models.propertyChangedEvent, self.modelPropertyChangedHandler);});} else{models.off(v.models.propertyChangedEvent, this.modelPropertyChangedHandler);} } return models; } //Collection prototype properties... proto = { /* Internal model property change event handler */ modelPropertyChangedHandler: function modelPropertyChangedHandler(event, data){this.trigger(event, data);}, /* Mutators */ //Sets the collection's data property to [models]. setModels: function setModels(models, options){ this.coll = []; addModels(this, models); this.isReadOnly = options && options.readOnly; return this; }, //Works like [].pop. Fires removeEvent if options.silent isn't passed or is false. Maintains deletedColl. pop: function pop(){ var m = removePropertyChangedEvents.call(this, this.coll.pop()); this.deletedColl.push(m); //0.6.3 Check for silent. if(!isSilent(arguments)){this.trigger(v.collections.removeEvent, m);} return m; }, //Works like [].push. Fires addEvent if options.silent isn't passed or it is false. push: function push(models){ var pushed = addModels(this, models); //0.6.3 Check for silent. if(!isSilent(arguments)){this.trigger(v.collections.addEvent, pushed);} return this.coll.length; }, //Works like [].reverse. reverse: function reverse(){this.coll.reverse();}, //Works like [].shift. Fires removeEvent if options.silent isn't passed or it is false. Maintains deletedColl. shift: function shift(){ var m = removePropertyChangedEvents.call(this, this.coll.shift()); this.deletedColl.push(m); //0.6.3 Check for silent. if(!isSilent(arguments)){this.trigger(v.collections.removeEvent, m);} return m; }, //Works like [].sort(function(a,b){...}). Fires sortEvent if options.silent isn't passed or it is false. sort: function sort(callback){ this.coll.sort(function(a, b){return callback(a, b);}); //0.6.3 Check for silent. if(!isSilent(arguments)){this.trigger(v.collections.sortEvent);} }, //Works like [].splice. Takes model or [modlels] as 3rd parameter. Maintains deletedColl. Fires addEvent and removeEvent if options.silent isn't passed or it is false. splice: function splice(index, howMany, models){ var a =[index, howMany], // aa = argsToModels(this, [].slice.call(arguments, 2)); aa = argsToModels(this, models); a = a.concat(aa); var m = removePropertyChangedEvents.call(this, [].splice.apply(this.coll, a)); if(m.length){this.deletedColl.push.apply(this.deletedColl, m);} //0.6.3 Check for silent. if(!isSilent(arguments) && aa && aa.length){this.trigger(v.collections.addEvent, aa);} //0.6.3 Check for silent. if(!isSilent(arguments) && howMany !== 0){this.trigger(v.collections.removeEvent, m);} return m; }, //Works like [].unshift. Takes model or [models] as 1st parameter. If raw data is passed it/they will first be converted to models. Fires addEvent if options.silent isn't passed or it is false. unshift: function unshift(models){ // var added = argsToModels(this, arguments), var added = argsToModels(this, models), l = [].unshift.apply(this.coll, added); //0.6.3 Check for silent. if(!isSilent(arguments)){this.trigger(v.collections.addEvent, added);} return l; }, /* Accessors */ //Returns an array of the deep copied data of all models in the collection getData: function getData(){return this.map(function(model){return model.getData();});}, //Works like array[i]. at: function at(index){return this.coll[index];}, //Find a model by its id and return it. findById: function findById(id){for(var i = 0, len = this.coll.length; i < len; i++){if(this.at(i).modelId === id){return this.at(i);}}}, //Works like [].slice. slice: function slice(){var self = this; return [].slice.apply(this.coll, arguments).map(function(model){return makeModelFromRaw(self, model.getData());});}, /* Iterators */ //Invokes a callback function for each model in the collection with three arguments: the model, the model's //index, and the collection object's coll. If supplied, the second parameter will be used as the context for //the callback. forEach: function forEach(/*callback, context*/){[].forEach.apply(this.coll, [].slice.call(arguments, 0));}, //Tests whether all models in the collection pass the test implemented by the provided callback. every: function every(/*callback, context*/){return [].every.apply(this.coll, [].slice.call(arguments, 0));}, //Tests whether some model in the collection passes the test implemented by the provided callback. some: function some(/*callback, context*/){return [].some.apply(this.coll, [].slice.call(arguments, 0));}, //Returns an array containing all the models that pass the test implemented by the provided callback. filter: function filter(/*callback, context*/){return [].filter.apply(this.coll, [].slice.call(arguments, 0));}, //Creates a new array with the results of calling a provided function on every model in the collection. map: function map(/*callback, context*/){return [].map.apply(this.coll, [].slice.call(arguments, 0));}, /* Sugar */ //Loads a collection with data by fetching the data from the server via ajax, uses modelsEndPoint as the url & returns a promise. v0.6.4 Added argument dataType and call to collection.parse(). fetch: function fetch(dataType){ var options = {url: this.modelsEndPoint}, deferred = v.$.Deferred(), self = this, promise; if(dataType){options.dataType = dataType;} promise = v.ajax.ajaxGet(options); //v0.6.2 return self added. promise.done(function(data){ //0.6.4. If data was returned call parse. data = data ? self.parse(data) : data; //If data was returned set this model's data. 0.6.4 Made call setModels conditional on data. if(data){self.setModels(data);} deferred.resolve(self); }); //v0.6.2 return self added. v0.6.4 Now returns jqXHR, textStatus & errorThrown promise.fail(function(jqXHR, textStatus, errorThrown){deferred.reject(self, jqXHR, textStatus, errorThrown);}); return deferred.promise(); }, //Sets the readOnly flag on all models in the collection to isReadOnly. setReadOnly: function setReadOnly(readOnly){this.coll.forEach(function(model){model.isReadOnly = readOnly;}); this.isReadOnly = readOnly;}, //Removes all models from the collection whose data properties matches those of matchingPropertiesHash. Any models removed from their //collection will also have their property changed event handlers removed. Removing models causes a remove event to be fired if //options.silent isn't passed or is false, and the removed models are passed along as the 2nd argument to the event handler's callback //function. Maintains deletedColl. remove: function remove(matchingPropertiesHash){ var removed = [], newColl; if(this.coll.length === 0 || isArrayOrNotObject(matchingPropertiesHash)){return;} newColl = this.coll.filter(function(el){ if(isMatch(el.data, matchingPropertiesHash)){removed.push(el); return false;} return true; }); if(removed.length){ this.deletedColl.push.apply(this.deletedColl, removePropertyChangedEvents.call(this, removed)); this.coll = newColl; //0.6.3 Check for silent. if(!isSilent(arguments)){this.trigger(v.collections.removeEvent, removed);} } return removed; }, //Returns true if the coll has at least one model whose data properties matches those of matchingPropertiesHash. has: function has(matchingPropertiesHash){ if(isArrayOrNotObject(matchingPropertiesHash)){return false;} return this.coll.some(function(el){return isMatch(el.data, matchingPropertiesHash);}); }, //Returns an array whose elements contain the stringified value of their model's data. find: function find(matchingPropertiesHash){ if(isArrayOrNotObject(matchingPropertiesHash)){return null;} return this.coll.filter(function(el){return isMatch(el.data, matchingPropertiesHash);}); }, //Stringifies all models data and returns them in an array. toJSON: function toJSON(){ return this.coll.map(function(el){return JSON.stringify(el.getData());}); }, //Same as Coccyx.collections.toRaw(models). See above for details. toRaw: toRaw, //Returns the length of the collection. getLength: function getLength(){return this.coll.length;}, //0.6.4 Override to transform the data returned from collection.fetch and return json. parse: function parse(data){return data;} }; v.collections = {extend: extend, toRaw: toRaw, addEvent: addEvent, removeEvent: removeEvent, sortEvent: sortEvent}; }); define('views', ['helpers', 'application'], function(){ 'use strict'; var v = window.Coccyx = window.Coccyx || {}, domEventTopic = 'DOM_EVENT', proto; //0.6.0 //Wire view dom events to callback methods in the controller using the context of the controller when //calling the callbacks. domEventsHash = {controller: controller, events: {'event selector': callback, ...}}. function wireDomEvents(domEventsHash, $domTarget, namespace){ var prop, a; for(prop in domEventsHash.events){ if(domEventsHash.events.hasOwnProperty(prop)){a = prop.split(' '); $domTarget.on(a[0] + namespace, a[1], domEventsHash.events[prop].bind(domEventsHash.controller));} } } //0.6.0 //Every view must have a domTarget property, which is used to render the view. Views can be rendered in 1 of 2 ways, either //"detached" or "attached" to the DOM. To render as detached, provide either a tagName property ('div', 'section', 'article', //'span', etc.) and a domTargetAttrs property (a hash, that can contain an id property, a class property and other element //attributes as properties), or set the domTarget property (either a string, whose value would be appropriate for calling //document.createElement(domTarget), or a callback to a function that returns a string appropriate for calling //document.createElement(domTarget)) yourself, or omit domTarget and domTargetAttrs altogether and a default domTarget will be //provided for you, which will be a plain "
" element. To render as attached, set $domTarget to a valid jquery object and //domTarget will be created for you from $domTarget. function setTarget(){ /*jshint validthis:true*/ if(this.$domTarget && this.$domTarget instanceof v.$){ //Use $domTarget. this.domTarget = this.$domTarget[0]; }else if(this.domTarget){ //Use domTarget. this.domTarget = document.createElement(typeof this.domTarget === 'string' ? this.domTarget : this.domTarget()); this.$domTarget = v.$(this.domTarget); }else if(this.domTargetAttrs){ //Use domTargetAttrs. this.$domTarget = v.$(document.createElement(this.tagName ? this.tagName : 'div')).attr(this.domTargetAttrs); this.domTarget = this.$domTarget[0]; }else{ //Default to 'div'. this.domTarget = document.createElement('div'); this.$domTarget = v.$(this.domTarget); } } //0.5.0, 0.6.0 function extend(viewObject, domEventsHash){ //Create a new object using the view object as its prototype. var obj1 = v.helpers.extend(Object.create(proto), viewObject), obj2 = Object.create(obj1); //0.6.0 Set domTarget && $domTarget setTarget.call(obj2); //0.6.0 Wire up events, if any are declared. if(domEventsHash){ obj2.namespace = '.' + Date.now().toString(); wireDomEvents(typeof domEventsHash === 'function' ? domEventsHash() : domEventsHash, obj2.$domTarget, obj2.namespace); } return obj2; } proto = {remove: function remove(){this.$domTarget.off(this.namespace); this.$domTarget.empty(); }, $: v.$}; v.views = {extend: extend, domEventTopic: domEventTopic}; }); define('coccyx', ['helpers', 'eventer', 'ajax', 'application', 'router', 'history', 'models', 'collections', 'views'], function () {'use strict'; return window.Coccyx;});