Created
January 30, 2012 11:45
-
-
Save DimitarChristoff/1703999 to your computer and use it in GitHub Desktop.
composer test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Composer.js is an MVC framework for creating and organizing javascript | |
* applications. For documentation, please visit: | |
* | |
* http://lyonbros.github.com/composer.js/ | |
* | |
* ----------------------------------------------------------------------------- | |
* | |
* Copyright (c) 2011, Lyon Bros Enterprises, LLC. (http://www.lyonbros.com) | |
* | |
* Licensed under The MIT License. | |
* Redistributions of files must retain the above copyright notice. | |
*/ | |
(function() { | |
var Composer = this.Composer = {}; | |
/** | |
* You must override this function in your app. | |
*/ | |
Composer.sync = function(method, model, options) { | |
return options.success(); | |
}; | |
// a closure that returns incrementing integers. these will be unique across | |
// the entire app since only one counter is instantiated | |
Composer.cid = (function() { | |
var counter = 1; | |
return function(inc) { | |
return 'c' + counter++; | |
}; | |
})(); | |
/** | |
* The base class is inherited by models, collections, and controllers. It | |
* provides some nice common functionality. | |
*/ | |
var Base = new Class({ | |
/** | |
* allows one object to extend another. since controllers, models, and | |
* collections all do this differently, it is up to each to have their own | |
* extend function and call this one for validation. | |
*/ | |
extend: function(obj, base) { | |
obj || (obj = {}); | |
base || (base = null); | |
if (obj.initialize) { | |
var str = 'You are creating a Composer object with an "initialize" method/' + 'parameter, which is reserved. Unless you know what you\'re doing ' + '(and call this.parent.apply(this, arguments)), please rename ' + 'your parameter to something other than "initialize"! Perhaps you' + 'were thinking of init()?'; | |
console.log('----------WARNING----------'); | |
console.log(str); | |
console.log('---------------------------'); | |
} | |
if (obj.extend) { | |
var str = 'You are creating a Composer object with an "extend" method/' + 'parameter, which is reserved. Unless you know what you\'re doing ' + '(and call this.parent.apply(this, arguments)), please rename ' + 'your parameter to something other than "extend"!'; | |
console.log('----------WARNING----------'); | |
console.log(str); | |
console.log('---------------------------'); | |
} | |
return obj; | |
}, | |
_do_extend: function(obj, base) { | |
var obj = Object.merge({ | |
Extends: (base || this.$constructor) | |
}, obj); | |
var cls = new Class(obj); | |
return cls; | |
}, | |
/** | |
* fire_event determines whether or not an event should fire. given an event | |
* name, the passed-in options, and any arbitrary number of arguments, | |
* determine whether or not the given event should be triggered. | |
*/ | |
fire_event: function() { | |
var args = shallow_array_clone(Array.from(arguments)); | |
var evname = args.shift(); | |
var options = args.shift(); | |
options || (options = {}); | |
// add event name back into the beginning of args | |
args.unshift(evname); | |
if (!options.silent && !options.not_silent) { | |
// not silent, fire the event | |
return this.fireEvent.apply(this, args); | |
} | |
else if ( | |
options.not_silent == evname || (options.not_silent && options.not_silent.length && options.not_silent.contains(evname))) { | |
// silent, BUT the given event is allowed. fire it. | |
return this.fireEvent.apply(this, args); | |
} | |
return this; | |
}.protect() | |
}); | |
/** | |
* Models are the data class. They deal with loading and manipulating data from | |
* various sources (ajax, local storage, etc). They make wrapping your actual | |
* data easy, and tie in well with collections/controllers via events to allow | |
* for easy updating and rendering. | |
* | |
* They also tie in with the Composer.sync function to provide a central place | |
* for saving/updating information with a server. | |
*/ | |
var Model = Composer.Model = new Class({ | |
Extends: Base, | |
Implements: [Events, Options], | |
// for internal object testing | |
__is_model: true, | |
// the model's unique app id, assigned by composer on instantiation | |
_cid: false, | |
options: {}, | |
// default values for the model, merged with the data passed in on CTOR | |
defaults: {}, | |
// holds the model's data | |
data: {}, | |
// whether or not the model has changed since the last save/update via sync | |
_changed: false, | |
// reference to the collections the model is in (yes, multiple). urls are | |
// pulled from the collection via a "priority" parameter. the highest | |
// priority collection will have its url passed to the model's sync function. | |
collections: [], | |
// what key to look under the data for the primary id for the object | |
id_key: 'id', | |
// can be used to overwrite all url generation for syncing (if you have a url | |
// that doesn't fit into the "/[collection url]/[model id]" scheme. | |
url: false, | |
// can be used to manually set a base url for this model (in the case it | |
// doesn't have a collection or the url needs to change manually). | |
base_url: false, | |
/** | |
* CTOR, allows passing in of data to set that data into the model. | |
*/ | |
initialize: function(data, options) { | |
this.setOptions(options); | |
data || (data = {}); | |
// merge the defaults into the data | |
data = Object.merge(Object.clone(this.defaults), data); | |
// assign the unique app id | |
this._cid = Composer.cid(); | |
// set the data into the model (but don't trigger any events) | |
this.set(data, { | |
silent: true | |
}); | |
// call the init fn | |
this.init(options); | |
}, | |
extend: function(obj, base) { | |
obj || (obj = {}); | |
base || (base = Model); | |
obj = this.parent.call(this, obj, base); | |
return this._do_extend(obj, base); | |
}, | |
/** | |
* override me, if needed | |
*/ | |
init: function() {}, | |
/** | |
* wrapper to get data out of the model. it's bad form to access model.data | |
* directly, you must always go through model.get('mykey') | |
*/ | |
get: function(key, def) { | |
if (typeof(def) == 'undefined') def = null; | |
if (typeof(this.data[key]) == 'undefined') { | |
return def; | |
} | |
return this.data[key]; | |
}, | |
/** | |
* like Model.get(), but if the data is a string, escape it for HTML output. | |
*/ | |
escape: function(key) { | |
var data = this.get(key); | |
if (data == null || typeof(data) != 'string') { | |
return data; | |
} | |
// taken directly from backbone.js's escapeHTML() function... thanks! | |
return data.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); | |
}, | |
/** | |
* whether or not a key exists in this.data | |
*/ | |
has: function(key) { | |
return this.data[key] != null; | |
}, | |
/** | |
* set data into the model. triggers change events for individual attributes | |
* that change, and also a general change event if the model has changed. it | |
* only triggers these events if the model has indeed changed, setting an | |
* attribute to the same value it currently is will not trigger events: | |
* | |
* model.set({name: "fisty", age: 21}); | |
* | |
* this will trigger the events: | |
* "change:name" | |
* "change:age" | |
* "change" | |
* | |
* if the model belongs to a collection, the events will bubble up to that | |
* collection as well, so as to notify the collection of any display changes | |
* needed. | |
*/ | |
set: function(data, options) { | |
options || (options = {}); | |
if (!options.silent && !this.perform_validation(data, options)) return false; | |
var already_changing = this.changing; | |
this.changing = true; | |
var self = this; | |
Object.each(data, function(val, key) { | |
if (!Composer.eq(val, self.data[key])) { | |
self.data[key] = val; | |
self._changed = true; | |
self.fireEvent('change:' + key, [val, options]); | |
} | |
}); | |
if (!already_changing && this._changed) { | |
this.fireEvent('change', [options]); | |
this._changed = false; | |
} | |
this.changing = false; | |
return this; | |
}, | |
/** | |
* unset a key from the model's data, triggering change events if needed. | |
*/ | |
unset: function(key, options) { | |
if (!(key in this.data)) return this; | |
options || (options = {}); | |
var obj = {}; | |
obj[key] = void(0); | |
if (!options.silent && !this.perform_validation(obj, options)) return false; | |
delete this.data[key]; | |
this._changed = true; | |
this.fireEvent('change:' + key, options, this, void 0, options); | |
this.fireEvent('change', options, this, options); | |
this._changed = false; | |
}, | |
/** | |
* clear all data out of a model, triggering change events if needed. | |
*/ | |
clear: function(options) { | |
options || (options = {}); | |
var old = this.data; | |
var obj = {}; | |
for (key in old) obj[key] = void(0); | |
if (!options.silent && !this.perform_validation(obj, options)) return false; | |
this.data = {}; | |
if (!options.silent) { | |
for (key in old) { | |
this._changed = true; | |
this.fireEvent('change' + key, options, this, void 0, options); | |
} | |
if (this._changed) { | |
this.fireEvent('change', options, this, options); | |
this._changed = false; | |
} | |
} | |
}, | |
/** | |
* fetch this model from the server, via its id. | |
*/ | |
fetch: function(options) { | |
options || (options = {}); | |
var success = options.success; | |
options.success = function(res) { | |
this.set(this.parse(res), options); | |
if (success) success(this, res); | |
}.bind(this); | |
options.error = wrap_error(options.error ? options.error.bind(this) : null, this, options).bind(this); | |
return (this.sync || Composer.sync).call(this, 'read', this, options); | |
}, | |
/** | |
* save this model to the server (update if exists, add if doesn't exist (uses | |
* id to detemrine if exists or note). | |
*/ | |
save: function(options) { | |
options || (options = {}); | |
if (!this.perform_validation(this.data, options)) return false; | |
var success = options.success; | |
options.success = function(res) { | |
if (!this.set(this.parse(res), options)) return false; | |
if (success) success(this, res); | |
}.bind(this); | |
options.error = wrap_error(options.error ? options.error.bind(this) : null, this, options).bind(this); | |
return (this.sync || Composer.sync).call(this, (this.is_new() ? 'create' : 'update'), this, options); | |
}, | |
/** | |
* delete this item from the server | |
*/ | |
destroy: function(options) { | |
options || (options = {}); | |
if (this.is_new()) { | |
return this.fireEvent('destroy', options, this, this.collections, options); | |
} | |
var success = options.success; | |
options.success = function(res) { | |
this.fireEvent('destroy', options, this, this.collections, options); | |
if (success) success(this, res); | |
}.bind(this); | |
options.error = wrap_error(options.error ? options.error.bind(this) : null, this, options).bind(this); | |
return (this.sync || Composer.sync).call(this, 'delete', this, options); | |
}, | |
/** | |
* overridable function that gets called when model data comes back from the | |
* server. use it to perform any needed transformations before setting data | |
* into the model. | |
*/ | |
parse: function(data) { | |
return data; | |
}, | |
/** | |
* get this model's id. if it doesn't exist, return the cid instead. | |
*/ | |
id: function(no_cid) { | |
if (typeof(no_cid) != 'boolean') no_cid = false; | |
var id = this.get(this.id_key); | |
if (id) return id; | |
if (no_cid) return false; | |
return this._cid; | |
}, | |
/** | |
* get the model's unique app id (cid) | |
*/ | |
cid: function() { | |
return this._cid; | |
}, | |
/** | |
* test whether or not the model is new (checks if it has an id) | |
*/ | |
is_new: function() { | |
return !this.id(true); | |
}, | |
/** | |
* create a new model with this models data and return it | |
*/ | |
clone: function() { | |
return new this.$constructor(this.toJSON()); | |
}, | |
/** | |
* return the raw data for this model (cloned, not referenced). | |
*/ | |
toJSON: function() { | |
return Object.clone(this.data); | |
}, | |
/** | |
* validate the model using its validation function (if it exists) | |
*/ | |
perform_validation: function(data, options) { | |
if (typeof(this.validate) != 'function') return true; | |
var error = this.validate(data, options); | |
if (error) { | |
if (options.error) { | |
options.error(this, error, options); | |
} | |
else { | |
this.fireEvent('error', options, this, error, options); | |
} | |
return false; | |
} | |
return true; | |
}, | |
/** | |
* loops over the collections this model belongs to and gets the highest | |
* priority one. makes for easier url extraction during syncing. | |
*/ | |
highest_priority_collection: function() { | |
var collections = shallow_array_clone(this.collections); | |
collections.sort(function(a, b) { | |
return b.priority - a.priority; | |
}); | |
return collections.length ? collections[0] : false; | |
}, | |
/** | |
* get the endpoint url for this model. | |
*/ | |
get_url: function() { | |
if (this.url) | |
// we are overriding the url generation. | |
return this.url; | |
// pull from either overridden "base_url" param, or just use the highest | |
// priority collection's url for the base. | |
if (this.base_url) var base_url = this.base_url; | |
else { | |
var collection = this.highest_priority_collection(); | |
// We need to check that there actually IS a collection... | |
if (collection) var base_url = collection.get_url(); | |
else var base_url = ''; | |
} | |
// create a /[base url]/[model id] url. | |
var id = this.id(true); | |
if (id) id = '/' + id; | |
else id = ''; | |
var url = base_url ? '/' + base_url.replace(/^\/+/, '').replace(/\/+$/, '') + id : id; | |
return url; | |
} | |
}); | |
/** | |
* Collections hold lists of models and contain various helper functions for | |
* finding and selecting subsets of model data. They are basically a wrapper | |
* around an array, thats function is dealing with large amounts of model data. | |
* | |
* Collections can also sync with the server like models. They tie into model | |
* events in such a way that if a model's data changes, the collection will be | |
* notified, and anybody listinging to the collection (ie, a controller) can | |
* react to that event (re-display the view, for instance). | |
*/ | |
var Collection = Composer.Collection = new Class({ | |
Extends: Base, | |
Implements: [Events, Options], | |
// the TYPE of model in this collection | |
model: Model, | |
// "private" array holding all the models in this collection | |
_models: [], | |
// function used for sorting. override to sort on a criteria besides order of | |
// addition to collection | |
sortfn: null, | |
// the base url for this collection. if you update a model, the default url | |
// sent to the sync function would be PUT /[collection url]/[model id]. | |
url: '/mycollection', | |
// when a model belongs to many collections, it will generate its url from the | |
// collection having the highest priority. if all have the same priority, then | |
// the first collection from the list will have its url used for the model's | |
// sync operation. | |
priority: 1, | |
/** | |
* allow the passing in of an array of data to instantiate a collection with a | |
* pre-set number of models. models will be created via this.model. | |
*/ | |
initialize: function(models, params, options) { | |
this.setOptions(options); | |
params || (params = {}); | |
for (x in params) { | |
this[x] = params[x]; | |
} | |
// allow Collection.model to be a string so load-order dependencies can be | |
// kept to a minimum. here, we convert the string to an object on collection | |
// instantiation and store it back into Collection.model. | |
// | |
// NOTE: this happens before the initial reset =] | |
this.model = typeof(this.model) == 'string' ? eval(this.model) : this.model; | |
if (models) { | |
this.reset(models, this.options); | |
} | |
this.init(); | |
}, | |
extend: function(obj, base) { | |
obj || (obj = {}); | |
base || (base = Collection); | |
obj = this.parent.call(this, obj, base); | |
return this._do_extend(obj, base); | |
}, | |
/** | |
* override me | |
*/ | |
init: function() {}, | |
/** | |
* for each model in this collection, get its raw data, then return all of the | |
* raw data in an array | |
*/ | |
toJSON: function() { | |
return this.models().map(function(model) { | |
return model.toJSON(); | |
}); | |
}, | |
/** | |
* wrapper to get the models under this collection for direct selection (often | |
* via MooTools' array helper/selection functions) | |
*/ | |
models: function() { | |
return this._models; | |
}, | |
/** | |
* add a model to this collection, and hook up the correct wire in doing so | |
* (events and setting the model's collection). | |
*/ | |
add: function(data, options) { | |
if (data instanceof Array) { | |
return Object.each(data, function(model) { | |
this.add(model, options) | |
}, this); | |
} | |
options || (options = {}); | |
// if we are passing raw data, create a new model from data | |
var model = data.__is_model ? data : new this.model(data, options); | |
// reference this collection to the model | |
if (!model.collections.contains(this)) { | |
model.collections.push(this); | |
} | |
if (this.sortfn) { | |
// if we have a sorting function, get the index the model should exist at | |
// and add it to that position | |
var index = options.at ? parseInt(options.at) : this.sort_index(model); | |
this._models.splice(index, 0, model); | |
} | |
else { | |
// no sort fn, add model to the end of the list | |
this._models.push(model); | |
} | |
// listen to the model's events so we can propogate them | |
model.bindEvent('all', this._model_event.bind(this)); | |
this.fireEvent('add', options, model, this, options); | |
}, | |
/** | |
* remove a model(s) from the collection, unhooking all necessary wires (events, etc) | |
*/ | |
remove: function(model, options) { | |
if (model instanceof Array) { | |
return Object.each(model, function(m) { | |
this.remove(m) | |
}, this); | |
} | |
options || (options = {}); | |
// remove this collection's reference(s) from the model | |
model.collections.erase(this); | |
// save to trigger change event if needed | |
var num_rec = this._models.length; | |
// remove hte model | |
this._models.erase(model); | |
// if the number actually change, trigger our change event | |
if (this._models.length != num_rec) { | |
this.fireEvent('remove', options, model); | |
} | |
// remove the model from the collection | |
this._remove_reference(model); | |
}, | |
/** | |
* remove all the models from the collection | |
*/ | |
clear: function(options) { | |
options || (options = {}); | |
// save to trigger change event if needed | |
var num_rec = this._models.length; | |
this._models.each(function(model) { | |
this._remove_reference(model); | |
}, this); | |
this._models = []; | |
// if the number actually change, trigger our change event | |
if (this._models.length != num_rec) { | |
this.fireEvent('clear', options); | |
} | |
}, | |
/** | |
* reset the collection with all new data. it can also be appended to the | |
* current set of models if specified in the options (via "append"). | |
*/ | |
reset: function(data, options) { | |
options || (options = {}); | |
if (!options.append) { | |
this.clear(options); | |
} | |
this.add(data, options); | |
this.fireEvent('reset', options); | |
}, | |
/** | |
* not normally necessary to call this, unless collection.sortfn changes after | |
* instantiation of the data. sort order is normall maintained upon adding of | |
* data viw Collection.add(). | |
*/ | |
sort: function(options) { | |
if (!this.sortfn) return false; | |
this._models.sort(this.sortfn); | |
this.fireEvent('reset', options, this, options); | |
}, | |
/** | |
* given the current for function and a model passecd in, determine the index | |
* the model should exist at in the colleciton's model list. | |
*/ | |
sort_index: function(model) { | |
if (!this.sortfn) return false; | |
for (var i = 0; i < this._models.length; i++) { | |
if (this.sortfn(this._models[i], model) > 0) { | |
return i; | |
} | |
} | |
return this._models.length; | |
}, | |
/** | |
* overridable function called when the collection is synced with the server | |
*/ | |
parse: function(data) { | |
return data; | |
}, | |
/** | |
* convenience function to loop over collection's models | |
*/ | |
each: function(cb, bind) { | |
if (bind) { | |
this.models().each(cb, bind); | |
} | |
else { | |
this.models().each(cb); | |
} | |
}, | |
/** | |
* Find the first model that satisfies the callback. An optional sort function | |
* can be passed in to order the results of the find, which uses the usual | |
* fn(a,b){return (-1|0|1);} syntax. | |
*/ | |
find: function(callback, sortfn) { | |
if (sortfn) { | |
var models = shallow_array_clone(this.models()).sort(sortfn); | |
} | |
else { | |
var models = this.models(); | |
} | |
for (var i = 0; i < models.length; i++) { | |
var rec = models[i]; | |
if (callback(rec)) { | |
return rec; | |
} | |
} | |
return false; | |
}, | |
/** | |
* given a callback, returns whether or not at least one of the models | |
* satisfies that callback. | |
*/ | |
exists: function(callback) { | |
return this.models().some(callback); | |
}, | |
/** | |
* convenience function to find a model by id | |
*/ | |
find_by_id: function(id) { | |
return this.find(function(model) { | |
if (model.id() == id) { | |
return true; | |
} | |
}); | |
}, | |
/** | |
* convenience function to find a model by cid | |
*/ | |
find_by_cid: function(cid) { | |
return this.find(function(model) { | |
if (model.cid() == cid) { | |
return true; | |
} | |
}); | |
}, | |
/** | |
* get the index of an item in the list of models. useful for sorting items. | |
*/ | |
index_of: function(model_or_id) { | |
var id = model_or_id.__is_model ? model_or_id.id() : model_or_id; | |
for (var i = 0; i < this._models.length; i++) { | |
if (this._models[i].id() == id) { | |
return i; | |
} | |
} | |
return false; | |
}, | |
/** | |
* query the models in the collection with a callback and return ALL that | |
* match. takes either a function OR a key-value object for matching: | |
* | |
* mycol.select(function(data) { | |
* if(data.get('name') == 'andrew' && data.get('age') == 24) | |
* { | |
* return true | |
* } | |
* }); | |
* | |
* is the same as: | |
* | |
* mycol.select({ | |
* name: andrew, | |
* age: 24 | |
* }); | |
* | |
* in other words, it's a very simple version of MongoDB's selection syntax, | |
* but with a lot less functionality. the only selection is direct value | |
* matching. still nice, though. | |
*/ | |
select: function(selector) { | |
if (typeof(selector) == 'object') { | |
var qry = []; | |
for (var key in selector) { | |
var val = selector[key]; | |
qry.push('data.get("' + key + '") == ' + val); | |
} | |
var fnstr = 'if(' + qry.join('&&') + ') { return true; }'; | |
selector = new Function('data', fnstr); | |
} | |
return this._models.filter(selector); | |
}, | |
/** | |
* return the first model in the collection. if n is specified, return the | |
* first n models. | |
*/ | |
first: function(n) { | |
var models = this.models(); | |
return (typeof(n) != 'undefined' && parseInt(n) != 0) ? models.slice(0, n) : models[0]; | |
}, | |
/** | |
* returns the last model in the collection. if n is specified, returns the | |
* last n models. | |
*/ | |
last: function(n) { | |
var models = this.models(); | |
return (typeof(n) != 'undefined' && parseInt(n) != 0) ? models.slice(models.length - n) : models[0]; | |
}, | |
/** | |
* sync the collection with the server. | |
*/ | |
fetch: function(options) { | |
options || (options = {}); | |
var success = options.success; | |
options.success = function(res) { | |
this.reset(this.parse(res), options); | |
if (success) success(this, res); | |
}.bind(this); | |
options.error = wrap_error(options.error ? options.error.bind(this) : null, this, options).bind(this); | |
return (this.sync || Composer.sync).call(this, 'read', this, options); | |
}, | |
/** | |
* simple wrapper to get the collection's url | |
*/ | |
get_url: function() { | |
return this.url; | |
}, | |
/** | |
* remove all ties between this colleciton and a model | |
*/ | |
_remove_reference: function(model) { | |
model.collections.erase(this); | |
// don't listen to this model anymore | |
model.unbind('all', this._model_event.bind(this)); | |
}, | |
/** | |
* bound to every model's "all" event, propagates or reacts to certain events. | |
*/ | |
_model_event: function(ev, model, collections, options) { | |
if ((ev == 'add' || ev == 'remove') && !collections.contains(this)) return; | |
if (ev == 'destroy') { | |
this.remove(model, options); | |
} | |
this.fireEvent.apply(this, arguments); | |
} | |
}); | |
/** | |
* The controller class sits between views and your models/collections. | |
* Controllers bind events to your data objects and update views when the data | |
* changes. Controllers are also responsible for rendering views. | |
*/ | |
var Controller = Composer.Controller = new Class({ | |
Extends: Base, | |
Implements: [Events, Options], | |
// the DOM element to tie this controller to (a container element) | |
el: false, | |
// if this is set to a DOM *selector*, then this.el will be ignored and | |
// instantiated as a new Element(this.tag), then injected into the element | |
// referened by the this.inject selector. this allows you to inject | |
// controllers into the DOM | |
inject: false, | |
// don't worry about it | |
event_splitter: /^(\w+)\s*(.*)$/, | |
// if tihs.el is empty, create a new element of this type as the container | |
tag: 'div', | |
// elements to assign to this controller | |
elements: {}, | |
// events to bind to this controllers sub-items. | |
events: {}, | |
/** | |
* CTOR. instantiate main container element (this.el), setup events and | |
* elements, and call init() | |
*/ | |
initialize: function(params) { | |
for (x in params) { | |
this[x] = params[x]; | |
} | |
// make sure we have an el | |
this._ensure_el(); | |
if (this.inject) { | |
this.attach(); | |
} | |
if (this.className) { | |
this.el.addClass(this.className); | |
} | |
this.refresh_elements(); | |
this.delegate_events(); | |
this.init(); | |
}, | |
extend: function(obj, base) { | |
obj || (obj = {}); | |
base || (base = Controller); | |
obj = this.parent.call(this, obj, base); | |
// extend the base object's events and elements | |
obj.events = Object.merge(this.events || {}, obj.events); | |
obj.elements = Object.merge(this.elements || {}, obj.elements); | |
return this._do_extend(obj, base); | |
}, | |
/** | |
* override | |
*/ | |
init: function() {}, | |
// lol | |
/** | |
* override. not OFFICIALLY used by the framework, but it's good to use it AND | |
* return "this" when you're done with it. | |
*/ | |
render: function() { | |
return this; | |
}, | |
/** | |
* replace this.el's html with the given test, also refresh the controllers | |
* elements. | |
*/ | |
html: function(str) { | |
if (!this.el) { | |
this._ensure_el(); | |
} | |
this.el.set('html', str); | |
this.refresh_elements(); | |
}, | |
/** | |
* injects to controller's element into the DOM. | |
*/ | |
attach: function(options) { | |
// make sure we have an el | |
this._ensure_el(); | |
var container = document.getElement(this.inject); | |
if (!container) { | |
return false; | |
} | |
container.set('html', ''); | |
this.el.inject(container); | |
}, | |
/** | |
* make sure el is defined as an HTML element | |
*/ | |
_ensure_el: function() { | |
// allow this.el to be a string selector (selecting a single element) instad | |
// of a DOM object. this allows the defining of a controller before the DOM | |
// element the selector refers to exists, but this.el will be updated upon | |
// instantiation of the controller (presumably when the DOM object DOES | |
// exist). | |
if (typeof(this.el) == 'string') { | |
this.el = document.getElement(this.el); | |
} | |
// if this.el is null (bad selector or no item given), create a new DOM | |
// object from this.tag | |
this.el || (this.el = new Element(this.tag)); | |
}, | |
/** | |
* remove the controller from the DOM and trigger its release event | |
*/ | |
release: function(options) { | |
options || (options = {}); | |
if (this.el && this.el.destroy) { | |
if (options.dispose) { | |
this.el.dispose(); | |
} | |
else { | |
this.el.destroy(); | |
} | |
} | |
this.el = false; | |
this.fireEvent('release', options, this); | |
}, | |
/** | |
* replace this controller's container element (this.el) with another element. | |
* also refreshes the events/elements associated with the controller | |
*/ | |
replace: function(element) { | |
if (this.el.parentNode) { | |
element.replaces(this.el); | |
} | |
this.el = element; | |
this.refresh_elements(); | |
this.delegate_events(); | |
return element; | |
}, | |
/** | |
* set up the events (by delegation) to this controller (events are stored | |
* under this.events). | |
*/ | |
delegate_events: function() { | |
// setup the events given | |
for (ev in this.events) { | |
var fn = this[this.events[ev]]; | |
if (typeof(fn) != 'function') { | |
// easy, easy, whoa, you gotta calm down there, chuck | |
continue; | |
} | |
fn = fn.bind(this); | |
match = ev.match(this.event_splitter); | |
var evname = match[1].trim(); | |
var selector = match[2].trim(); | |
if (selector == '') { | |
this.el.removeEvent(evname, fn); | |
this.el.addEvent(evname, fn); | |
} | |
else { | |
this.el.addEvent(evname + ':relay(' + selector + ')', fn); | |
} | |
} | |
}, | |
/** | |
* re-init the elements into the scope of the controller (uses this.elements) | |
*/ | |
refresh_elements: function() { | |
// setup given elements as instance variables | |
for (selector in this.elements) { | |
var iname = this.elements[selector]; | |
this[iname] = this.el.getElement(selector); | |
} | |
} | |
}); | |
/* | |
--- | |
description: Added the onhashchange event | |
license: MIT-style | |
authors: | |
- sdf1981cgn | |
- Greggory Hernandez | |
requires: | |
- core/1.2.4: '*' | |
provides: [Element.Events.hashchange] | |
... | |
*/ | |
Element.Events.hashchange = { | |
onAdd: function() { | |
var hash = self.location.hash; | |
var hashchange = function() { | |
if (hash == self.location.hash) return; | |
else hash = self.location.hash; | |
var value = (hash.indexOf('#') == 0 ? hash.substr(1) : hash); | |
window.fireEvent('hashchange', value); | |
document.fireEvent('hashchange', value); | |
}; | |
if ("onhashchange" in window) { | |
window.onhashchange = hashchange; | |
} else { | |
hashchange.periodical(50); | |
} | |
} | |
}; | |
var Router = Composer.Router = new Class({ | |
last_hash: false, | |
routes: {}, | |
callbacks: [], | |
Implements: [Options, Events], | |
options: { | |
redirect_initial: false, | |
suppress_initial_route: false, | |
enable_cb: function() { | |
return true; | |
}, | |
on_failure: function() {}, | |
base_url: '/#!' | |
}, | |
/** | |
* initialize the routes your app uses. this is really the only public | |
* function that exists in the router, since it takes care of everything for | |
* you after instantiation. | |
*/ | |
initialize: function(routes, options) { | |
this.setOptions(options); | |
this.routes = routes; | |
this.register_callback(this._do_route.bind(this)); | |
// load the initial hash value | |
var hash = self.location.hash; | |
var value = (hash.indexOf('#') == 0 ? hash.substr(1) : hash); | |
// if redirect_initial is true, then whatever page a user lands on, redirect | |
// them to the hash version, ie | |
// | |
// gonorrhea.com/users/display/42 | |
// becomes: | |
// gonorrhea.com/#!/users/display/42 | |
// | |
// the routing system will pick this new hash up after the redirect and route | |
// it normally | |
if (this.options.redirect_initial && hash.trim() == '' && location.protocol !== 'file:') { | |
window.location = this.options.baseURL + self.location.pathname; | |
} | |
// set up the hashchange event | |
window.addEvent('hashchange', this.hash_change.bind(this)); | |
if (!this.options.suppress_initial_route) { | |
// run the initial route | |
window.fireEvent('hashchange', [value]); | |
} | |
}, | |
/** | |
* run the given callback when a route changes | |
*/ | |
register_callback: function(cb) { | |
this.callbacks.push(cb); | |
}, | |
/** | |
* wrapper around the routing functionality. basically, instead of doing a | |
* window.location = '#!/my/route'; | |
* you can do | |
* router.route('#!/my/route'); | |
* | |
* Note that the latter isn't necessary, but it provides a useful abstraction. | |
*/ | |
route: function(url) { | |
url || (url = new String(window.location.href)); | |
var href = url.trim(); | |
href = '/' + href.replace(/^[a-z]+:\/\/.*?\//, '').replace(/^[#!\/]+/, ''); | |
var hash = '#!' + href; | |
var old = new String(self.location.hash).toString(); | |
if (old == hash) { | |
window.fireEvent('hashchange', [href, true]); | |
} | |
else { | |
window.location = hash; | |
} | |
}, | |
/** | |
* given a url, route it within the given routes the router was instantiated | |
* with. if none fit, do nothing =] | |
* | |
* *internal only* =] | |
*/ | |
_do_route: function(url) { | |
if (!this.options.enable_cb()) { | |
return false; | |
} | |
var url = '/' + url.replace(/^!?\//g, ''); | |
var route = false; | |
var match = []; | |
for (var re in this.routes) { | |
var regex = '/^' + re.replace(/\//g, '\\\/') + '$/'; | |
match = eval(regex).exec(url); | |
if (match) { | |
route = this.routes[re]; | |
break; | |
} | |
} | |
if (!route) return this.options.on_failure({ | |
url: url, | |
route: false, | |
handler_exists: false, | |
action_exists: false | |
}); | |
var handler = route[0]; | |
var action = route[1]; | |
if (!window[handler]) return this.options.on_failure({ | |
url: url, | |
route: route, | |
handler_exists: false, | |
action_exists: false | |
}); | |
var obj = window[handler]; | |
if (!obj[action] || typeof(obj[action]) != 'function') return this.options.on_failure({ | |
url: url, | |
route: route, | |
handler_exists: true, | |
action_exists: false | |
}); | |
var args = match; | |
args.shift(); | |
obj[action].apply(obj, args); | |
}, | |
/** | |
* stupid function, not worth the space it takes up | |
*/ | |
setup_routes: function(routes) { | |
this.routes = routes; | |
}, | |
/** | |
* attached to the hashchange event. runs all the callback assigned with | |
* register_callback(). | |
*/ | |
hash_change: function(hash, force) { | |
var force = !! force; | |
// remove the motherfucking ! at the beginning | |
hash = hash.replace(/^!/, ''); | |
if (this.last_hash == hash && !force) { | |
// no need to reload | |
return false; | |
} | |
this.last_hash = hash; | |
this.fireEvent("hashchange", hash); | |
this.callbacks.each(function(fn) { | |
if (typeof(fn) == 'function') fn.call(this, hash); | |
}, this); | |
} | |
}); | |
// wraps error callbacks for syncing functions | |
var wrap_error = function(callback, model, options) { | |
return function(resp) { | |
if (callback) { | |
callback(model, resp, options); | |
} | |
else { | |
this.fireEvent('error', options, model, resp, options); | |
} | |
}; | |
}; | |
// do a shallow clone of an array | |
var shallow_array_clone = function(from) { | |
var to = new Array(); | |
for (i in from) { | |
to[i] = from[i]; | |
} | |
return to; | |
}; | |
// taken and modified from underscore.js. added in some function helpers for | |
// the value comparisons. | |
// | |
// Underscore.js 1.2.0 | |
// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. | |
// Underscore is freely distributable under the MIT license. | |
// Portions of Underscore are inspired or borrowed from Prototype, | |
// Oliver Steele's Functional, and John Resig's Micro-Templating. | |
// For all details and documentation: | |
// http://documentcloud.github.com/underscore | |
Composer.eq = function(a, b, stack) { | |
stack || (stack = []); | |
// Identical objects are equal. `0 === -0`, but they aren't identical. | |
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. | |
if (a === b) return a !== 0 || 1 / a == 1 / b; | |
// A strict comparison is necessary because `null == undefined`. | |
if (a == null) return a === b; | |
// Compare object types. | |
var typeA = typeof a; | |
if (typeA != typeof b) return false; | |
// Optimization; ensure that both values are truthy or falsy. | |
if (!a != !b) return false; | |
// `NaN` values are equal. | |
if (a != a) return b != b; | |
// Compare string objects by value. | |
var is_string = function(obj) { | |
return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); | |
}; | |
var isStringA = is_string(a), | |
isStringB = is_string(b); | |
if (isStringA || isStringB) return isStringA && isStringB && String(a) == String(b); | |
// Compare number objects by value. | |
var is_number = function(obj) { | |
return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed)); | |
}; | |
var isNumberA = is_number(a), | |
isNumberB = is_number(b); | |
if (isNumberA || isNumberB) return isNumberA && isNumberB && +a == +b; | |
// Compare boolean objects by value. The value of `true` is 1; the value of `false` is 0. | |
var is_boolean = function(b) { | |
return b === true || b === false; | |
}; | |
var isBooleanA = is_boolean(a), | |
isBooleanB = is_boolean(b); | |
if (isBooleanA || isBooleanB) return isBooleanA && isBooleanB && +a == +b; | |
// Ensure that both values are objects. | |
if (typeA != 'object') return false; | |
// Unwrap any wrapped objects. | |
if (a._chain) a = a._wrapped; | |
if (b._chain) b = b._wrapped; | |
// Assume equality for cyclic structures. The algorithm for detecting cyclic structures is | |
// adapted from ES 5.1 section 15.12.3, abstract operation `JO`. | |
var length = stack.length; | |
while (length--) { | |
// Linear search. Performance is inversely proportional to the number of unique nested | |
// structures. | |
if (stack[length] == a) return true; | |
} | |
// Add the first object to the stack of traversed objects. | |
var hasOwnProperty = Object.prototype.hasOwnProperty; | |
stack.push(a); | |
var size = 0, | |
result = true; | |
if (a.length === +a.length || b.length === +b.length) { | |
// Compare object lengths to determine if a deep comparison is necessary. | |
size = a.length; | |
result = size == b.length; | |
if (result) { | |
// Deep compare array-like object contents, ignoring non-numeric properties. | |
while (size--) { | |
// Ensure commutative equality for sparse arrays. | |
if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; | |
} | |
} | |
} else { | |
// Deep compare objects. | |
for (var key in a) { | |
if (hasOwnProperty.call(a, key)) { | |
// Count the expected number of properties. | |
size++; | |
// Deep compare each member. | |
if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break; | |
} | |
} | |
// Ensure that both objects contain the same number of properties. | |
if (result) { | |
for (key in b) { | |
if (hasOwnProperty.call(b, key) && !size--) break; | |
} | |
result = !size; | |
} | |
} | |
// Remove the first object from the stack of traversed objects. | |
stack.pop(); | |
return result; | |
}; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment