Commit 466307fa authored by RYan Eastridge's avatar RYan Eastridge

initial thorax todos commit

parent 83cf863f
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Thorax • TodoMVC</title>
<link rel="stylesheet" href="../../../assets/base.css">
<!--[if IE]>
<script src="../../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
<script type="text/template" data-template-name="app">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
{{^empty todosCollection}}
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
{{#collection todosCollection item-view="todo-item" tag="ul" id="todo-list"}}
<div class="view">
<input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{title}}">
{{/collection}}
</section>
{{view "stats" tag="footer" id="footer"}}
{{/empty}}
</section>
<div id="info">
<p>Double-click to edit a todo</p>
<p>Written by <a href="https://github.com/addyosmani">Addy Osmani</a> &amp; <a href="https://github.com/beastridge">Ryan Eastridge</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</div>
</script>
<script type="text/template" data-template-name="stats">
<span id="todo-count"><strong>{{remaining}}</strong> {{itemText}} left</span>
<ul id="filters">
<li>
{{#link "/" class="selected"}}All{{/link}}
</li>
<li>
{{#link "/active"}}Active{{/link}}
</li>
<li>
{{#link "/completed"}}Completed{{/link}}
</li>
</ul>
{{#if completed}}
<button id="clear-completed">Clear completed ({{completed}})</button>
{{/if}}
</script>
<script src="../../../assets/base.js"></script>
<script src="../../../assets/jquery.min.js"></script>
<script src="../../../assets/lodash.min.js"></script>
<script src="../../../assets/handlebars.min.js"></script>
<script src="js/lib/backbone-min.js"></script>
<script src="js/lib/backbone-localstorage.js"></script>
<script src="js/lib/thorax.js"></script>
<script>
// Grab the text from the templates we created above
Thorax.templates = {
app: Handlebars.compile($('script[data-template-name="app"]').html()),
stats: Handlebars.compile($('script[data-template-name="stats"]').html())
};
</script>
<script src="js/models/todo.js"></script>
<script src="js/collections/todos.js"></script>
<script src="js/views/todo-item.js"></script>
<script src="js/views/stats.js"></script>
<script src="js/views/app.js"></script>
<script src="js/routers/router.js"></script>
<script src="js/app.js"></script>
</body>
</html>
var app = app || {};
var ENTER_KEY = 13;
$(function() {
// Kick things off by creating the **App**.
var view = new Thorax.Views['app']();
$('body').append(view.el);
});
var app = app || {};
(function() {
'use strict';
// Todo Collection
// ---------------
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var TodoList = Backbone.Collection.extend({
// Reference to this collection's model.
model: app.Todo,
// Save all of the todo items under the `"todos"` namespace.
localStorage: new Store('todos-backbone'),
// Filter down the list of all todo items that are finished.
completed: function() {
return this.filter(function( todo ) {
return todo.get('completed');
});
},
// Filter down the list to only todo items that are still not finished.
remaining: function() {
return this.without.apply( this, this.completed() );
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder: function() {
if ( !this.length ) {
return 1;
}
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator: function( todo ) {
return todo.get('order');
}
});
// Create our global collection of **Todos**.
app.Todos = new TodoList();
}());
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Generate four random hex digits.
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};
// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
var Store = function(name) {
this.name = name;
var store = localStorage.getItem(this.name);
this.data = (store && JSON.parse(store)) || {};
};
_.extend(Store.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function() {
localStorage.setItem(this.name, JSON.stringify(this.data));
},
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own.
create: function(model) {
if (!model.id) model.id = model.attributes.id = guid();
this.data[model.id] = model;
this.save();
return model;
},
// Update a model by replacing its copy in `this.data`.
update: function(model) {
this.data[model.id] = model;
this.save();
return model;
},
// Retrieve a model from `this.data` by id.
find: function(model) {
return this.data[model.id];
},
// Return the array of all models currently in storage.
findAll: function() {
return _.values(this.data);
},
// Delete a model from `this.data`, returning it.
destroy: function(model) {
delete this.data[model.id];
this.save();
return model;
}
});
// Override `Backbone.sync` to use delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
Backbone.sync = function(method, model, options) {
var resp;
var store = model.localStorage || model.collection.localStorage;
switch (method) {
case "read": resp = model.id ? store.find(model) : store.findAll(); break;
case "create": resp = store.create(model); break;
case "update": resp = store.update(model); break;
case "delete": resp = store.destroy(model); break;
}
if (resp) {
options.success(resp);
} else {
options.error("Record not found");
}
};
// Backbone.js 0.9.2
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);
// Copyright (c) 2011-2012 @WalmartLabs
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
(function() {
var Thorax;
//support zepto.forEach on jQuery
if (!$.fn.forEach) {
$.fn.forEach = function(iterator, context) {
$.fn.each.call(this, function(index) {
iterator.call(context || this, this, index);
});
}
}
if (typeof exports !== 'undefined') {
Thorax = exports;
} else {
Thorax = this.Thorax = {};
}
Thorax.VERSION = '2.0.0b3';
var handlebarsExtension = 'handlebars',
handlebarsExtensionRegExp = new RegExp('\\.' + handlebarsExtension + '$'),
viewNameAttributeName = 'data-view-name',
viewCidAttributeName = 'data-view-cid',
viewPlaceholderAttributeName = 'data-view-tmp',
viewHelperAttributeName = 'data-view-helper',
elementPlaceholderAttributeName = 'data-element-tmp';
_.extend(Thorax, {
templatePathPrefix: '',
//view instances
_viewsIndexedByCid: {},
templates: {},
Views: {}
});
Thorax.Util = {
createRegistryWrapper: function(klass, hash) {
var $super = klass.extend;
klass.extend = function() {
var child = $super.apply(this, arguments);
if (child.prototype.name) {
hash[child.prototype.name] = child;
}
return child;
};
},
registryGet: function(object, type, name, ignoreErrors) {
if (type === 'templates') {
//append the template path prefix if it is missing
var pathPrefix = Thorax.templatePathPrefix;
if (pathPrefix && pathPrefix.length && name && name.substr(0, pathPrefix.length) !== pathPrefix) {
name = pathPrefix + name;
}
}
var target = object[type],
value;
if (name.match(/\./)) {
var bits = name.split(/\./);
name = bits.pop();
bits.forEach(function(key) {
target = target[key];
});
} else {
value = target[name];
}
if (!target && !ignoreErrors) {
throw new Error(type + ': ' + name + ' does not exist.');
} else {
var value = target[name];
if (type === 'templates' && typeof value === 'string') {
value = target[name] = Handlebars.compile(value);
}
return value;
}
},
getViewInstance: function(name, attributes) {
attributes['class'] && (attributes.className = attributes['class']);
attributes.tag && (attributes.tagName = attributes.tag);
if (typeof name === 'string') {
var klass = Thorax.Util.registryGet(Thorax, 'Views', name, false);
return klass.cid ? _.extend(klass, attributes || {}) : new klass(attributes);
} else if (typeof name === 'function') {
return new name(attributes);
} else {
return name;
}
},
getValue: function (object, prop) {
if (!(object && object[prop])) {
return null;
}
return _.isFunction(object[prop])
? object[prop].apply(object, Array.prototype.slice.call(arguments, 2))
: object[prop];
},
//'selector' is not present in $('<p></p>')
//TODO: investigage a better detection method
is$: function(obj) {
return typeof obj === 'object' && ('length' in obj);
},
expandToken: function(input, scope) {
if (input && input.indexOf && input.indexOf('{' + '{') >= 0) {
var re = /(?:\{?[^{]+)|(?:\{\{([^}]+)\}\})/g,
match,
ret = [];
function deref(token, scope) {
var segments = token.split('.'),
len = segments.length;
for (var i = 0; scope && i < len; i++) {
if (segments[i] !== 'this') {
scope = scope[segments[i]];
}
}
return scope;
}
while (match = re.exec(input)) {
if (match[1]) {
var params = match[1].split(/\s+/);
if (params.length > 1) {
var helper = params.shift();
params = params.map(function(param) { return deref(param, scope); });
if (Handlebars.helpers[helper]) {
ret.push(Handlebars.helpers[helper].apply(scope, params));
} else {
// If the helper is not defined do nothing
ret.push(match[0]);
}
} else {
ret.push(deref(params[0], scope));
}
} else {
ret.push(match[0]);
}
}
input = ret.join('');
}
return input;
},
tag: function(attributes, content, scope) {
var htmlAttributes = _.clone(attributes),
tag = htmlAttributes.tag || htmlAttributes.tagName || 'div';
if (htmlAttributes.tag) {
delete htmlAttributes.tag;
}
if (htmlAttributes.tagName) {
delete htmlAttributes.tagName;
}
return '<' + tag + ' ' + _.map(htmlAttributes, function(value, key) {
if (typeof value === 'undefined') {
return '';
}
var formattedValue = value;
if (scope) {
formattedValue = Thorax.Util.expandToken(value, scope);
}
return key + '="' + Handlebars.Utils.escapeExpression(formattedValue) + '"';
}).join(' ') + '>' + (typeof content === 'undefined' ? '' : content) + '</' + tag + '>';
},
htmlAttributesFromOptions: function(options) {
var htmlAttributes = {};
if (options.tag) {
htmlAttributes.tag = options.tag;
}
if (options.tagName) {
htmlAttributes.tagName = options.tagName;
}
if (options['class']) {
htmlAttributes['class'] = options['class'];
}
if (options.id) {
htmlAttributes.id = options.id;
}
return htmlAttributes;
},
_cloneEvents: function(source, target, key) {
source[key] = _.clone(target[key]);
//need to deep clone events array
_.each(source[key], function(value, _key) {
if (_.isArray(value)) {
target[key][_key] = _.clone(value);
}
});
}
};
Thorax.View = Backbone.View.extend({
constructor: function() {
var response = Thorax.View.__super__.constructor.apply(this, arguments);
if (this.model) {
//need to null this.model so setModel will not treat
//it as the old model and immediately return
var model = this.model;
this.model = null;
this.setModel(model);
}
return response;
},
_configure: function(options) {
this._modelEvents = [];
this._collectionEvents = [];
Thorax._viewsIndexedByCid[this.cid] = this;
this.children = {};
this._renderCount = 0;
//this.options is removed in Thorax.View, we merge passed
//properties directly with the view and template context
_.extend(this, options || {});
//compile a string if it is set as this.template
if (typeof this.template === 'string') {
this.template = Handlebars.compile(this.template);
} else if (this.name && !this.template) {
//fetch the template
this.template = Thorax.Util.registryGet(Thorax, 'templates', this.name, true);
}
//HelperView will not have mixins so need to check
if (this.constructor.mixins) {
//mixins
for (var i = 0; i < this.constructor.mixins.length; ++i) {
applyMixin.call(this, this.constructor.mixins[i]);
}
if (this.mixins) {
for (var i = 0; i < this.mixins.length; ++i) {
applyMixin.call(this, this.mixins[i]);
}
}
}
//_events not present on HelperView
this.constructor._events && this.constructor._events.forEach(function(event) {
this.on.apply(this, event);
}, this);
if (this.events) {
_.each(Thorax.Util.getValue(this, 'events'), function(handler, eventName) {
this.on(eventName, handler, this);
}, this);
}
},
_ensureElement : function() {
Backbone.View.prototype._ensureElement.call(this);
if (this.name) {
this.$el.attr(viewNameAttributeName, this.name);
}
this.$el.attr(viewCidAttributeName, this.cid);
},
_addChild: function(view) {
this.children[view.cid] = view;
if (!view.parent) {
view.parent = this;
}
return view;
},
destroy: function(options) {
options = _.defaults(options || {}, {
children: true
});
this.trigger('destroyed');
delete Thorax._viewsIndexedByCid[this.cid];
_.each(this.children, function(child) {
if (options.children) {
child.parent = null;
child.destroy();
}
});
if (options.children) {
this.children = {};
}
},
render: function(output) {
if (typeof output === 'undefined' || (!_.isElement(output) && !Thorax.Util.is$(output) && !(output && output.el) && typeof output !== 'string' && typeof output !== 'function')) {
if (!this.template) {
//if the name was set after the view was created try one more time to fetch a template
if (this.name) {
this.template = Thorax.Util.registryGet(Thorax, 'templates', this.name, true);
}
if (!this.template) {
throw new Error('View ' + (this.name || this.cid) + '.render() was called with no content and no template set on the view.');
}
}
output = this.renderTemplate(this.template);
} else if (typeof output === 'function') {
output = this.renderTemplate(output);
}
//accept a view, string, Handlebars.SafeString or DOM element
this.html((output && output.el) || (output && output.string) || output);
++this._renderCount;
this.trigger('rendered');
return output;
},
context: function() {
return this;
},
_getContext: function(attributes) {
return _.extend({}, Thorax.Util.getValue(this, 'context'), attributes || {}, {
cid: _.uniqueId('t'),
_view: this
});
},
renderTemplate: function(file, data, ignoreErrors) {
var template;
data = this._getContext(data);
if (typeof file === 'function') {
template = file;
} else {
template = this._loadTemplate(file);
}
if (!template) {
if (ignoreErrors) {
return ''
} else {
throw new Error('Unable to find template ' + file);
}
} else {
return template(data);
}
},
_loadTemplate: function(file, ignoreErrors) {
return Thorax.Util.registryGet(Thorax, 'templates', file, ignoreErrors);
},
ensureRendered: function() {
!this._renderCount && this.render();
},
html: function(html) {
if (typeof html === 'undefined') {
return this.el.innerHTML;
} else {
var element = this.$el.html(html);
this._appendViews();
this._appendElements();
return element;
}
}
});
Thorax.View.extend = function() {
var child = Backbone.View.extend.apply(this, arguments);
child.mixins = _.clone(this.mixins);
Thorax.Util._cloneEvents(this, child, '_events');
Thorax.Util._cloneEvents(this, child, '_modelEvents');
Thorax.Util._cloneEvents(this, child, '_collectionEvents');
return child;
};
Thorax.Util.createRegistryWrapper(Thorax.View, Thorax.Views);
//helpers
Handlebars.registerHelper('super', function() {
var parent = this._view.constructor && this._view.constructor.__super__;
if (parent) {
var template = parent.template;
if (!template) {
if (!parent.name) {
throw new Error('Cannot use super helper when parent has no name or template.');
}
template = Thorax.Util.registryGet(Thorax, 'templates', parent.name, false);
}
if (typeof template === 'string') {
template = Handlebars.compile(template);
}
return new Handlebars.SafeString(template(this));
} else {
return '';
}
});
Handlebars.registerHelper('template', function(name, options) {
var context = _.extend({}, this, options ? options.hash : {});
var output = Thorax.View.prototype.renderTemplate.call(this._view, name, context);
return new Handlebars.SafeString(output);
});
//view helper
var viewTemplateOverrides = {};
Handlebars.registerHelper('view', function(view, options) {
if (arguments.length === 1) {
options = view;
view = Thorax.View;
}
var instance = Thorax.Util.getViewInstance(view, options ? options.hash : {}),
placeholder_id = instance.cid + '-' + _.uniqueId('placeholder');
this._view._addChild(instance);
this._view.trigger('child', instance);
if (options.fn) {
viewTemplateOverrides[placeholder_id] = options.fn;
}
var htmlAttributes = Thorax.Util.htmlAttributesFromOptions(options.hash);
htmlAttributes[viewPlaceholderAttributeName] = placeholder_id;
return new Handlebars.SafeString(Thorax.Util.tag.call(this, htmlAttributes));
});
Thorax.HelperView = Thorax.View.extend({
_ensureElement: function() {
Thorax.View.prototype._ensureElement.apply(this, arguments);
this.$el.attr(viewHelperAttributeName, this._helperName);
},
context: function() {
return this.parent.context.apply(this.parent, arguments);
}
});
//ensure nested inline helpers will always have this.parent
//set to the view containing the template
function getParent(parent) {
while (parent._helperName) {
parent = parent.parent;
}
return parent;
}
Handlebars.registerViewHelper = function(name, viewClass, callback) {
if (arguments.length === 2) {
options = {};
callback = arguments[1];
viewClass = Thorax.HelperView;
}
Handlebars.registerHelper(name, function() {
var args = _.toArray(arguments),
options = args.pop(),
viewOptions = {
template: options.fn,
inverse: options.inverse,
options: options.hash,
parent: getParent(this._view),
_helperName: name
};
options.hash.id && (viewOptions.id = options.hash.id);
options.hash['class'] && (viewOptions.className = options.hash['class']);
options.hash.className && (viewOptions.className = options.hash.className);
options.hash.tag && (viewOptions.tagName = options.hash.tag);
options.hash.tagName && (viewOptions.tagName = options.hash.tagName);
var instance = new viewClass(viewOptions);
args.push(instance);
this._view.children[instance.cid] = instance;
this._view.trigger.apply(this._view, ['helper', name].concat(args));
this._view.trigger.apply(this._view, ['helper:' + name].concat(args));
var htmlAttributes = Thorax.Util.htmlAttributesFromOptions(options.hash);
htmlAttributes[viewPlaceholderAttributeName] = instance.cid;
callback.apply(this, args);
return new Handlebars.SafeString(Thorax.Util.tag(htmlAttributes, ''));
});
var helper = Handlebars.helpers[name];
return helper;
};
//called from View.prototype.html()
Thorax.View.prototype._appendViews = function(scope, callback) {
(scope || this.$el).find('[' + viewPlaceholderAttributeName + ']').forEach(function(el) {
var placeholder_id = el.getAttribute(viewPlaceholderAttributeName),
cid = placeholder_id.replace(/\-placeholder\d+$/, ''),
view = this.children[cid];
//if was set with a helper
if (_.isFunction(view)) {
view = view.call(this._view);
}
if (view) {
//see if the view helper declared an override for the view
//if not, ensure the view has been rendered at least once
if (viewTemplateOverrides[placeholder_id]) {
view.render(viewTemplateOverrides[placeholder_id](view._getContext()));
} else {
view.ensureRendered();
}
$(el).replaceWith(view.el);
//TODO: jQuery has trouble with delegateEvents() when
//the child dom node is detached then re-attached
if (typeof jQuery !== 'undefined' && $ === jQuery) {
if (this._renderCount > 1) {
view.delegateEvents();
}
}
callback && callback(view.el);
}
}, this);
};
//element helper
Handlebars.registerHelper('element', function(element, options) {
var cid = _.uniqueId('element'),
htmlAttributes = Thorax.Util.htmlAttributesFromOptions(options.hash);
htmlAttributes[elementPlaceholderAttributeName] = cid;
this._view._elementsByCid || (this._view._elementsByCid = {});
this._view._elementsByCid[cid] = element;
return new Handlebars.SafeString(Thorax.Util.tag.call(this, htmlAttributes));
});
Thorax.View.prototype._appendElements = function(scope, callback) {
(scope || this.$el).find('[' + elementPlaceholderAttributeName + ']').forEach(function(el) {
var cid = el.getAttribute(elementPlaceholderAttributeName),
element = this._elementsByCid[cid];
if (_.isFunction(element)) {
element = element.call(this._view);
}
$(el).replaceWith(element);
callback && callback(element);
}, this);
};
//$(selector).view() helper
$.fn.view = function(options) {
options = _.defaults(options || {}, {
helper: true
});
var selector = '[' + viewCidAttributeName + ']';
if (!options.helper) {
selector += ':not([' + viewHelperAttributeName + '])';
}
var el = $(this).closest(selector);
return (el && Thorax._viewsIndexedByCid[el.attr(viewCidAttributeName)]) || false;
};
_.extend(Thorax.View, {
mixins: [],
mixin: function(mixin) {
this.mixins.push(mixin);
}
});
function applyMixin(mixin) {
if (_.isArray(mixin)) {
this.mixin.apply(this, mixin);
} else {
this.mixin(mixin);
}
}
var _destroy = Thorax.View.prototype.destroy,
_on = Thorax.View.prototype.on,
_delegateEvents = Thorax.View.prototype.delegateEvents;
_.extend(Thorax.View, {
_events: [],
on: function(eventName, callback) {
if (eventName === 'model' && typeof callback === 'object') {
return addEvents(this._modelEvents, callback);
}
if (eventName === 'collection' && typeof callback === 'object') {
return addEvents(this._collectionEvents, callback);
}
//accept on({"rendered": handler})
if (typeof eventName === 'object') {
_.each(eventName, function(value, key) {
this.on(key, value);
}, this);
} else {
//accept on({"rendered": [handler, handler]})
if (_.isArray(callback)) {
callback.forEach(function(cb) {
this._events.push([eventName, cb]);
}, this);
//accept on("rendered", handler)
} else {
this._events.push([eventName, callback]);
}
}
return this;
}
});
_.extend(Thorax.View.prototype, {
freeze: function(options) {
options = _.defaults(options || {}, {
dom: true,
children: true
});
this._eventArgumentsToUnbind && this._eventArgumentsToUnbind.forEach(function(args) {
args[0].off(args[1], args[2], args[3]);
});
this._eventArgumentsToUnbind = [];
this.off();
if (options.dom) {
this.undelegateEvents();
}
this.trigger('freeze');
if (options.children) {
_.each(this.children, function(child, id) {
child.freeze(options);
}, this);
}
},
destroy: function() {
var response = _destroy.apply(this, arguments);
this.freeze();
return response;
},
on: function(eventName, callback, context) {
if (eventName === 'model' && typeof callback === 'object') {
return addEvents(this._modelEvents, callback);
}
if (eventName === 'collection' && typeof callback === 'object') {
return addEvents(this._collectionEvents, callback);
}
if (typeof eventName === 'object') {
//accept on({"rendered": callback})
if (arguments.length === 1) {
_.each(eventName, function(value, key) {
this.on(key, value, this);
}, this);
//events on other objects to auto dispose of when view frozen
//on(targetObj, 'eventName', callback, context)
} else if (arguments.length > 1) {
if (!this._eventArgumentsToUnbind) {
this._eventArgumentsToUnbind = [];
}
var args = Array.prototype.slice.call(arguments);
this._eventArgumentsToUnbind.push(args);
args[0].on.apply(args[0], args.slice(1));
}
} else {
//accept on("rendered", callback, context)
//accept on("click a", callback, context)
(_.isArray(callback) ? callback : [callback]).forEach(function(callback) {
var params = eventParamsFromEventItem.call(this, eventName, callback, context || this);
if (params.type === 'DOM') {
//will call _addEvent during delegateEvents()
if (!this._eventsToDelegate) {
this._eventsToDelegate = [];
}
this._eventsToDelegate.push(params);
} else {
this._addEvent(params);
}
}, this);
}
return this;
},
delegateEvents: function(events) {
this.undelegateEvents();
if (events) {
if (_.isFunction(events)) {
events = events.call(this);
}
this._eventsToDelegate = [];
this.on(events);
}
this._eventsToDelegate && this._eventsToDelegate.forEach(this._addEvent, this);
},
//params may contain:
//- name
//- originalName
//- selector
//- type "view" || "DOM"
//- handler
_addEvent: function(params) {
if (params.type === 'view') {
params.name.split(/\s+/).forEach(function(name) {
_on.call(this, name, params.handler, params.context || this);
}, this);
} else {
var boundHandler = containHandlerToCurentView(bindEventHandler.call(this, params.handler), this.cid);
if (params.selector) {
//TODO: determine why collection views and some nested views
//need defered event delegation
var name = params.name + '.delegateEvents' + this.cid;
if (typeof jQuery !== 'undefined' && $ === jQuery) {
_.defer(_.bind(function() {
this.$el.on(name, params.selector, boundHandler);
}, this));
} else {
this.$el.on(name, params.selector, boundHandler);
}
} else {
this.$el.on(name, boundHandler);
}
}
}
});
var eventSplitter = /^(\S+)(?:\s+(.+))?/;
var domEvents = [
'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout',
'touchstart', 'touchend', 'touchmove',
'click', 'dblclick',
'keyup', 'keydown', 'keypress',
'submit', 'change',
'focus', 'blur'
];
var domEventRegexp = new RegExp('^(' + domEvents.join('|') + ')');
function containHandlerToCurentView(handler, cid) {
return function(event) {
var view = $(event.target).view({helper: false});
if (view && view.cid == cid) {
handler(event);
}
}
}
function bindEventHandler(callback) {
var method = typeof callback === 'function' ? callback : this[callback];
if (!method) {
throw new Error('Event "' + callback + '" does not exist');
}
return _.bind(method, this);
}
function eventParamsFromEventItem(name, handler, context) {
var params = {
originalName: name,
handler: typeof handler === 'string' ? this[handler] : handler
};
if (name.match(domEventRegexp)) {
var match = eventSplitter.exec(name);
params.name = match[1];
params.type = 'DOM';
params.selector = match[2];
} else {
params.name = name;
params.type = 'view';
}
params.context = context;
return params;
}
var modelCidAttributeName = 'data-model-cid',
modelNameAttributeName = 'data-model-name',
_freeze = Thorax.View.prototype.freeze,
_context = Thorax.View.prototype.context;
Thorax.Model = Backbone.Model.extend({
isEmpty: function() {
return this.isPopulated();
},
isPopulated: function() {
// We are populated if we have attributes set
var attributes = _.clone(this.attributes);
var defaults = _.isFunction(this.defaults) ? this.defaults() : (this.defaults || {});
for (var default_key in defaults) {
if (attributes[default_key] != defaults[default_key]) {
return true;
}
delete attributes[default_key];
}
var keys = _.keys(attributes);
return keys.length > 1 || (keys.length === 1 && keys[0] !== 'id');
}
});
Thorax.Models = {};
Thorax.Util.createRegistryWrapper(Thorax.Model, Thorax.Models);
Thorax.View._modelEvents = [];
function addEvents(target, source) {
_.each(source, function(callback, eventName) {
if (_.isArray(callback)) {
callback.forEach(function(cb) {
target.push([eventName, cb]);
}, this);
} else {
target.push([eventName, callback]);
}
});
}
_.extend(Thorax.View.prototype, {
_getContext: function(attributes) {
return _.extend({}, Thorax.Util.getValue(this, 'context', this.model), attributes || {}, {
cid: _.uniqueId('t'),
_view: this
});
},
context: function() {
return _.extend({}, _context.call(this), (this.model && this.model.attributes) || {});
},
freeze: function(options) {
this.model && this._unbindModelEvents();
_freeze.call(this, options);
},
_bindModelEvents: function() {
bindModelEvents.call(this, this.constructor._modelEvents);
bindModelEvents.call(this, this._modelEvents);
},
_unbindModelEvents: function() {
this.model.trigger('freeze');
unbindModelEvents.call(this, this.constructor._modelEvents);
unbindModelEvents.call(this, this._modelEvents);
},
setModel: function(model, options) {
var oldModel = this.model;
if (model === oldModel) {
return this;
}
oldModel && this._unbindModelEvents();
if (model) {
this.$el.attr(modelCidAttributeName, model.cid);
if (model.name) {
this.$el.attr(modelNameAttributeName, model.name);
}
this.model = model;
this._setModelOptions(options);
this._bindModelEvents(options);
this.model.trigger('set', this.model, oldModel);
if (Thorax.Util.shouldFetch(this.model, this._modelOptions)) {
var success = this._modelOptions.success;
this._loadModel(this.model, this._modelOptions);
} else {
//want to trigger built in event handler (render() + populate())
//without triggering event on model
this._onModelChange();
}
} else {
this._modelOptions = false;
this.model = false;
this._onModelChange();
this.$el.removeAttr(modelCidAttributeName);
this.$el.attr(modelNameAttributeName);
}
return this;
},
_onModelChange: function() {
if (!this._modelOptions || (this._modelOptions && this._modelOptions.render)) {
this.render();
}
},
_loadModel: function(model, options) {
model.fetch(options);
},
_setModelOptions: function(options) {
if (!this._modelOptions) {
this._modelOptions = {
fetch: true,
success: false,
render: true,
errors: true
};
}
_.extend(this._modelOptions, options || {});
return this._modelOptions;
}
});
function getEventCallback(callback, context) {
if (typeof callback === 'function') {
return callback;
} else {
return context[callback];
}
}
function bindModelEvents(events) {
events.forEach(function(event) {
//getEventCallback will resolve if it is a string or a method
//and return a method
this.model.on(event[0], getEventCallback(event[1], this), event[2] || this);
}, this);
}
function unbindModelEvents(events) {
events.forEach(function(event) {
this.model.off(event[0], getEventCallback(event[1], this), event[2] || this);
}, this);
}
Thorax.View.on({
model: {
error: function(model, errors){
if (this._modelOptions.errors) {
this.trigger('error', errors);
}
},
change: function() {
this._onModelChange();
}
}
});
Thorax.Util.shouldFetch = function(modelOrCollection, options) {
var getValue = Thorax.Util.getValue,
isCollection = !modelOrCollection.collection && modelOrCollection._byCid && modelOrCollection._byId;
url = (
(!modelOrCollection.collection && getValue(modelOrCollection, 'urlRoot')) ||
(modelOrCollection.collection && getValue(modelOrCollection.collection, 'url')) ||
(isCollection && getValue(modelOrCollection, 'url'))
);
return url && options.fetch && !(
(modelOrCollection.isPopulated && modelOrCollection.isPopulated()) ||
(isCollection
? Thorax.Collection && Thorax.Collection.prototype.isPopulated.call(modelOrCollection)
: Thorax.Model.prototype.isPopulated.call(modelOrCollection)
)
);
};
$.fn.model = function() {
var $this = $(this),
modelElement = $this.closest('[' + modelCidAttributeName + ']'),
modelCid = modelElement && modelElement.attr(modelCidAttributeName);
if (modelCid) {
var view = $this.view();
if (view && view.model && view.model.cid === modelCid) {
return view.model || false;
}
var collection = $this.collection(view);
if (collection) {
return collection._byCid[modelCid] || false;
}
}
return false;
};
var _fetch = Backbone.Collection.prototype.fetch,
_reset = Backbone.Collection.prototype.reset,
collectionCidAttributeName = 'data-collection-cid',
collectionNameAttributeName = 'data-collection-name',
collectionEmptyAttributeName = 'data-collection-empty',
modelCidAttributeName = 'data-model-cid',
modelNameAttributeName = 'data-model-name',
ELEMENT_NODE_TYPE = 1;
Thorax.Collection = Backbone.Collection.extend({
model: Thorax.Model || Backbone.Model,
isEmpty: function() {
if (this.length > 0) {
return false;
} else {
return this.length === 0 && this.isPopulated();
}
},
isPopulated: function() {
return this._fetched || this.length > 0 || (!this.length && !Thorax.Util.getValue(this, 'url'));
},
fetch: function(options) {
options = options || {};
var success = options.success;
options.success = function(collection, response) {
collection._fetched = true;
success && success(collection, response);
};
return _fetch.apply(this, arguments);
},
reset: function(models, options) {
this._fetched = !!models;
return _reset.call(this, models, options);
}
});
Thorax.Collections = {};
Thorax.Util.createRegistryWrapper(Thorax.Collection, Thorax.Collections);
Thorax.View._collectionEvents = [];
//collection view is meant to be initialized via the collection
//helper but can alternatively be initialized programatically
//constructor function handles this case, no logic except for
//super() call will be exectued when initialized via collection helper
Thorax.CollectionView = Thorax.HelperView.extend({
constructor: function(options) {
Thorax.CollectionView.__super__.constructor.call(this, options);
//collection helper will initialize this.options, so need to mimic
this.options || (this.options = {});
this.collection && this.setCollection(this.collection);
Thorax.CollectionView._optionNames.forEach(function(optionName) {
options[optionName] && (this.options[optionName] = options[optionName]);
}, this);
},
_setCollectionOptions: function(collection, options) {
return _.extend({
fetch: true,
success: false,
errors: true
}, options || {});
},
setCollection: function(collection, options) {
this.collection = collection;
if (collection) {
collection.cid = collection.cid || _.uniqueId('collection');
this.$el.attr(collectionCidAttributeName, collection.cid);
if (collection.name) {
this.$el.attr(collectionNameAttributeName, collection.name);
}
this.options = this._setCollectionOptions(collection, _.extend({}, this.options, options));
bindCollectionEvents.call(this, collection, this.parent._collectionEvents);
bindCollectionEvents.call(this, collection, this.parent.constructor._collectionEvents);
collection.trigger('set', collection);
if (Thorax.Util.shouldFetch(collection, this.options)) {
this._loadCollection(collection);
} else {
//want to trigger built in event handler (render())
//without triggering event on collection
this.reset();
}
//if we rendered with item views model changes will be observed
//by the generated item view but if we rendered with templates
//then model changes need to be bound as nothing is watching
if (!this.options['item-view']) {
this.on(collection, 'change', function(model) {
this.$el.find('[' + modelCidAttributeName + '="' + model.cid +'"]').remove();
this.appendItem(model, collection.indexOf(model));
}, this);
}
}
return this;
},
_loadCollection: function(collection) {
collection.fetch(this.options);
},
//appendItem(model [,index])
//appendItem(html_string, index)
//appendItem(view, index)
appendItem: function(model, index, options) {
//empty item
if (!model) {
return;
}
var itemView;
options = options || {};
//if index argument is a view
if (index && index.el) {
index = this.$el.children().indexOf(index.el) + 1;
}
//if argument is a view, or html string
if (model.el || typeof model === 'string') {
itemView = model;
model = false;
} else {
index = index || this.collection.indexOf(model) || 0;
itemView = this.renderItem(model, index);
}
if (itemView) {
if (itemView.cid) {
this._addChild(itemView);
}
//if the renderer's output wasn't contained in a tag, wrap it in a div
//plain text, or a mixture of top level text nodes and element nodes
//will get wrapped
if (typeof itemView === 'string' && !itemView.match(/^\s*\</m)) {
itemView = '<div>' + itemView + '</div>'
}
var itemElement = itemView.el ? [itemView.el] : _.filter($(itemView), function(node) {
//filter out top level whitespace nodes
return node.nodeType === ELEMENT_NODE_TYPE;
});
if (model) {
$(itemElement).attr(modelCidAttributeName, model.cid);
}
var previousModel = index > 0 ? this.collection.at(index - 1) : false;
if (!previousModel) {
this.$el.prepend(itemElement);
} else {
//use last() as appendItem can accept multiple nodes from a template
this.$el.find('[' + modelCidAttributeName + '="' + previousModel.cid + '"]').last().after(itemElement);
}
this._appendViews(null, function(el) {
el.setAttribute(modelCidAttributeName, model.cid);
});
this._appendElements(null, function(el) {
el.setAttribute(modelCidAttributeName, model.cid);
});
if (!options.silent) {
this.parent.trigger('rendered:item', this, this.collection, model, itemElement, index);
}
}
return itemView;
},
reset: function() {
this.render();
},
render: function() {
this.$el.empty();
if (this.collection) {
if (this.collection.isEmpty()) {
this.$el.attr(collectionEmptyAttributeName, true);
this.appendEmpty();
} else {
this.$el.removeAttr(collectionEmptyAttributeName);
this.collection.forEach(function(item, i) {
if (!this.options.filter || this.options.filter &&
(typeof this.options.filter === 'string'
? this.parent[this.options.filter]
: this.options.filter).call(this.parent, item, i)
) {
this.appendItem(item, i);
}
}, this);
}
this.parent.trigger('rendered:collection', this, this.collection);
}
++this._renderCount;
},
renderEmpty: function() {
var viewOptions = {};
if (this.options['empty-view']) {
if (this.options['empty-context']) {
viewOptions.context = _.bind(function() {
return (_.isFunction(this.options['empty-context'])
? this.options['empty-context']
: this.parent[this.options['empty-context']]
).call(this.parent);
}, this);
}
var view = Thorax.Util.getViewInstance(this.options['empty-view'], viewOptions);
if (this.options['empty-template']) {
view.render(this.renderTemplate(this.options['empty-template'], viewOptions.context ? viewOptions.context() : {}));
} else {
view.render();
}
return view;
} else {
var emptyTemplate = this.options['empty-template'] || (this.parent.name && this._loadTemplate(this.parent.name + '-empty', true));
var context;
if (this.options['empty-context']) {
context = (_.isFunction(this.options['empty-context'])
? this.options['empty-context']
: this.parent[this.options['empty-context']]
).call(this.parent);
} else {
context = {};
}
return emptyTemplate && this.renderTemplate(emptyTemplate, context);
}
},
renderItem: function(model, i) {
if (this.options['item-view']) {
var viewOptions = {
model: model
};
//itemContext deprecated
if (this.options['item-context']) {
viewOptions.context = _.bind(function() {
return (_.isFunction(this.options['item-context'])
? this.options['item-context']
: this.parent[this.options['item-context']]
).call(this.parent, model, i);
}, this);
}
if (this.options['item-template']) {
viewOptions.template = this.options['item-template'];
}
var view = Thorax.Util.getViewInstance(this.options['item-view'], viewOptions);
view.ensureRendered();
return view;
} else {
var itemTemplate = this.options['item-template'] || (this.parent.name && this.parent._loadTemplate(this.parent.name + '-item', true));
if (!itemTemplate) {
throw new Error('collection helper in View: ' + (this.parent.name || this.parent.cid) + ' requires an item template.');
}
var context;
if (this.options['item-context']) {
context = (_.isFunction(this.options['item-context'])
? this.options['item-context']
: this.parent[this.options['item-context']]
).call(this.parent, model, i);
} else {
context = model.attributes;
}
return this.renderTemplate(itemTemplate, context);
}
},
appendEmpty: function() {
this.$el.empty();
var emptyContent = this.renderEmpty();
emptyContent && this.appendItem(emptyContent, 0, {
silent: true
});
this.parent.trigger('rendered:empty', this, this.collection);
}
});
Thorax.CollectionView._optionNames = [
'item-template',
'empty-template',
'item-view',
'empty-view',
'item-context',
'empty-context',
'filter'
];
function bindCollectionEvents(collection, events) {
events.forEach(function(event) {
this.on(collection, event[0], function() {
//getEventCallback will resolve if it is a string or a method
//and return a method
var args = _.toArray(arguments);
args.unshift(this);
return getEventCallback(event[1], this.parent).apply(this.parent, args);
}, this);
}, this);
}
Thorax.View.on({
collection: {
add: function(collectionView, model, collection) {
if (collection.length === 1) {
if(collectionView.$el.length) {
collectionView.$el.removeAttr(collectionEmptyAttributeName);
collectionView.$el.empty();
}
}
if (collectionView.$el.length) {
var index = collection.indexOf(model);
if (!collectionView.options.filter || collectionView.options.filter &&
(typeof collectionView.options.filter === 'string'
? this[collectionView.options.filter]
: collectionView.options.filter).call(this, model, index)
) {
collectionView.appendItem(model, index);
}
}
},
remove: function(collectionView, model, collection) {
collectionView.$el.find('[' + modelCidAttributeName + '="' + model.cid + '"]').remove();
for (var cid in collectionView.children) {
if (collectionView.children[cid].model && collectionView.children[cid].model.cid === model.cid) {
collectionView.children[cid].destroy();
delete collectionView.children[cid];
break;
}
}
if (collection.length === 0) {
if (collectionView.$el.length) {
collectionView.$el.attr(collectionEmptyAttributeName, true);
collectionView.appendEmpty();
}
}
},
reset: function(collectionView, collection) {
collectionView.reset();
},
error: function(collectionView, message) {
if (collectionView.options.errors) {
collectionView.trigger('error', message);
this.trigger('error', message);
}
}
}
});
Handlebars.registerViewHelper('collection', Thorax.CollectionView, function(collection, view) {
if (arguments.length === 1) {
view = collection;
collection = this._view.collection;
}
if (collection) {
//item-view and empty-view may also be passed, but have no defaults
_.extend(view.options, {
'item-template': view.template && view.template !== Handlebars.VM.noop ? view.template : view.options['item-template'],
'empty-template': view.inverse && view.inverse !== Handlebars.VM.noop ? view.inverse : view.options['empty-template'],
'item-context': view.options['item-context'] || view.parent.itemContext,
'empty-context': view.options['empty-context'] || view.parent.emptyContext,
filter: view.options['filter']
});
view.setCollection(collection);
}
});
//empty helper
Handlebars.registerViewHelper('empty', function(collection, view) {
var empty, noArgument;
if (arguments.length === 1) {
view = collection;
collection = false;
noArgument = true;
}
var _render = view.render;
view.render = function() {
if (noArgument) {
empty = !this.parent.model || (this.parent.model && !this.parent.model.isEmpty());
} else if (!collection) {
empty = true;
} else {
empty = collection.isEmpty();
}
if (empty) {
this.parent.trigger('rendered:empty', this, collection);
return _render.call(this, this.template);
} else {
return _render.call(this, this.inverse);
}
};
//no model binding is necessary as model.set() will cause re-render
if (collection) {
function collectionRemoveCallback() {
if (collection.length === 0) {
view.render();
}
}
function collectionAddCallback() {
if (collection.length === 1) {
view.render();
}
}
function collectionResetCallback() {
view.render();
}
view.on(collection, 'remove', collectionRemoveCallback);
view.on(collection, 'add', collectionAddCallback);
view.on(collection, 'reset', collectionResetCallback);
}
view.render();
});
//$(selector).collection() helper
$.fn.collection = function(view) {
var $this = $(this),
collectionElement = $this.closest('[' + collectionCidAttributeName + ']'),
collectionCid = collectionElement && collectionElement.attr(collectionCidAttributeName);
if (collectionCid) {
view = view || $this.view();
if (view) {
return view.collection;
}
}
return false;
};
var paramMatcher = /:(\w+)/g,
callMethodAttributeName = 'data-call-method';
Handlebars.registerHelper('url', function(url) {
var matches = url.match(paramMatcher),
context = this;
if (matches) {
url = url.replace(paramMatcher, function(match, key) {
return context[key] ? Thorax.Util.getValue(context, key) : match;
});
}
url = Thorax.Util.expandToken(url, context);
return (Backbone.history._hasPushState ? Backbone.history.options.root : '#') + url;
});
Handlebars.registerHelper('button', function(method, options) {
options.hash.tag = options.hash.tag || options.hash.tagName || 'button';
options.hash[callMethodAttributeName] = method;
return new Handlebars.SafeString(Thorax.Util.tag.call(this, options.hash, options.fn ? options.fn(this) : '', this));
});
Handlebars.registerHelper('link', function(url, options) {
options.hash.tag = options.hash.tag || options.hash.tagName || 'a';
options.hash.href = Handlebars.helpers.url.call(this, url);
options.hash[callMethodAttributeName] = '_anchorClick';
return new Handlebars.SafeString(Thorax.Util.tag.call(this, options.hash, options.fn ? options.fn(this) : '', this));
});
$(function() {
$(document).on('click', '[' + callMethodAttributeName + ']', function(event) {
var target = $(event.target),
view = target.view({helper: false}),
methodName = target.attr(callMethodAttributeName);
view[methodName].call(view, event);
});
});
Thorax.View.prototype._anchorClick = function(event) {
var target = $(event.currentTarget),
href = target.attr('href');
// Route anything that starts with # or / (excluding //domain urls)
if (href && (href[0] === '#' || (href[0] === '/' && href[1] !== '/'))) {
Backbone.history.navigate(href, {
trigger: true
});
event.preventDefault();
}
};
if (Thorax.View.prototype._setModelOptions) {
(function() {
var _onModelChange = Thorax.View.prototype._onModelChange,
_setModelOptions = Thorax.View.prototype._setModelOptions;
_.extend(Thorax.View.prototype, {
_onModelChange: function() {
var response = _onModelChange.call(this);
if (this._modelOptions.populate) {
this.populate(this.model.attributes);
}
return response;
},
_setModelOptions: function(options) {
if (!options) {
options = {};
}
if (!('populate' in options)) {
options.populate = true;
}
return _setModelOptions.call(this, options);
}
});
})();
}
_.extend(Thorax.View.prototype, {
//serializes a form present in the view, returning the serialized data
//as an object
//pass {set:false} to not update this.model if present
//can pass options, callback or event in any order
serialize: function() {
var callback, options, event;
//ignore undefined arguments in case event was null
for (var i = 0; i < arguments.length; ++i) {
if (typeof arguments[i] === 'function') {
callback = arguments[i];
} else if (typeof arguments[i] === 'object') {
if ('stopPropagation' in arguments[i] && 'preventDefault' in arguments[i]) {
event = arguments[i];
} else {
options = arguments[i];
}
}
}
if (event && !this._preventDuplicateSubmission(event)) {
return;
}
options = _.extend({
set: true,
validate: true
},options || {});
var attributes = options.attributes || {};
//callback has context of element
var view = this;
var errors = [];
eachNamedInput.call(this, options, function() {
var value = view._getInputValue(this, options, errors);
if (typeof value !== 'undefined') {
objectAndKeyFromAttributesAndName.call(this, attributes, this.name, {mode: 'serialize'}, function(object, key) {
if (!object[key]) {
object[key] = value;
} else if (_.isArray(object[key])) {
object[key].push(value);
} else {
object[key] = [object[key], value];
}
});
}
});
this.trigger('serialize', attributes, options);
if (options.validate) {
var validateInputErrors = this.validateInput(attributes);
if (validateInputErrors && validateInputErrors.length) {
errors = errors.concat(validateInputErrors);
}
this.trigger('validate', attributes, errors, options);
if (errors.length) {
this.trigger('error', errors);
return;
}
}
if (options.set && this.model) {
if (!this.model.set(attributes, {silent: true})) {
return false;
};
}
callback && callback.call(this, attributes, _.bind(resetSubmitState, this));
return attributes;
},
_preventDuplicateSubmission: function(event, callback) {
event.preventDefault();
var form = $(event.target);
if ((event.target.tagName || '').toLowerCase() !== 'form') {
// Handle non-submit events by gating on the form
form = $(event.target).closest('form');
}
if (!form.attr('data-submit-wait')) {
form.attr('data-submit-wait', 'true');
if (callback) {
callback.call(this, event);
}
return true;
} else {
return false;
}
},
//populate a form from the passed attributes or this.model if present
populate: function(attributes) {
var value, attributes = attributes || this._getContext(this.model);
//callback has context of element
eachNamedInput.call(this, {}, function() {
objectAndKeyFromAttributesAndName.call(this, attributes, this.name, {mode: 'populate'}, function(object, key) {
if (object && typeof (value = object[key]) !== 'undefined') {
//will only execute if we have a name that matches the structure in attributes
if (this.type === 'checkbox' && _.isBoolean(value)) {
this.checked = value;
} else if (this.type === 'checkbox' || this.type === 'radio') {
this.checked = value == this.value;
} else {
this.value = value;
}
}
});
});
this.trigger('populate', attributes);
},
//perform form validation, implemented by child class
validateInput: function(attributes, options, errors) {},
_getInputValue: function(input, options, errors) {
if (input.type === 'checkbox' || input.type === 'radio') {
if (input.checked) {
return input.value;
}
} else if (input.multiple === true) {
var values = [];
$('option',input).each(function(){
if (this.selected) {
values.push(this.value);
}
});
return values;
} else {
return input.value;
}
}
});
Thorax.View.on({
error: function() {
resetSubmitState.call(this);
// If we errored with a model we want to reset the content but leave the UI
// intact. If the user updates the data and serializes any overwritten data
// will be restored.
if (this.model && this.model.previousAttributes) {
this.model.set(this.model.previousAttributes(), {
silent: true
});
}
},
deactivated: function() {
resetSubmitState.call(this);
}
})
function eachNamedInput(options, iterator, context) {
var i = 0;
this.$('select,input,textarea', options.root || this.el).each(function() {
if (this.type !== 'button' && this.type !== 'cancel' && this.type !== 'submit' && this.name && this.name !== '') {
iterator.call(context || this, i, this);
++i;
}
});
}
//calls a callback with the correct object fragment and key from a compound name
function objectAndKeyFromAttributesAndName(attributes, name, options, callback) {
var key, i, object = attributes, keys = name.split('['), mode = options.mode;
for(i = 0; i < keys.length - 1; ++i) {
key = keys[i].replace(']','');
if (!object[key]) {
if (mode == 'serialize') {
object[key] = {};
} else {
return callback.call(this, false, key);
}
}
object = object[key];
}
key = keys[keys.length - 1].replace(']', '');
callback.call(this, object, key);
}
function resetSubmitState() {
this.$('form').removeAttr('data-submit-wait');
}
//Router
function initializeRouter() {
Backbone.history || (Backbone.history = new Backbone.History);
Backbone.history.on('route', onRoute, this);
//router does not have a built in destroy event
//but ViewController does
this.on('destroyed', function() {
Backbone.history.off('route', onRoute, this);
});
}
Thorax.Router = Backbone.Router.extend({
constructor: function() {
var response = Thorax.Router.__super__.constructor.apply(this, arguments);
initializeRouter.call(this);
return response;
},
route: function(route, name, callback) {
//add a route:before event that is fired before the callback is called
return Backbone.Router.prototype.route.call(this, route, name, function() {
this.trigger.apply(this, ['route:before', name].concat(Array.prototype.slice.call(arguments)));
return callback.apply(this, arguments);
});
}
});
Thorax.Routers = {};
Thorax.Util.createRegistryWrapper(Thorax.Router, Thorax.Routers);
function onRoute(router, name) {
if (this === router) {
this.trigger.apply(this, ['route'].concat(Array.prototype.slice.call(arguments, 1)));
}
}
//layout
var layoutCidAttributeName = 'data-layout-cid';
Thorax.LayoutView = Thorax.View.extend({
render: function(output) {
//TODO: fixme, lumbar inserts templates after JS, most of the time this is fine
//but Application will be created in init.js (unlike most views)
//so need to put this here so the template will be picked up
var layoutTemplate;
if (this.name) {
layoutTemplate = Thorax.Util.registryGet(Thorax, 'templates', this.name, true);
}
//a template is optional in a layout
if (output || this.template || layoutTemplate) {
//but if present, it must have embedded an element containing layoutCidAttributeName
var response = Thorax.View.prototype.render.call(this, output || this.template || layoutTemplate);
ensureLayoutViewsTargetElement.call(this);
return response;
} else {
ensureLayoutCid.call(this);
}
},
setView: function(view, options) {
options = _.extend({
scroll: true,
destroy: true
}, options || {});
if (typeof view === 'string') {
view = new (Thorax.Util.registryGet(Thorax, 'Views', view, false));
}
this.ensureRendered();
var oldView = this._view;
if (view == oldView){
return false;
}
if (options.destroy && view) {
view._shouldDestroyOnNextSetView = true;
}
this.trigger('change:view:start', view, oldView, options);
oldView && oldView.trigger('deactivated', options);
view && view.trigger('activated', options);
if (oldView && oldView.el && oldView.el.parentNode) {
oldView.$el.remove();
}
//make sure the view has been rendered at least once
view && this._addChild(view);
view && view.ensureRendered();
view && getLayoutViewsTargetElement.call(this).appendChild(view.el);
this._view = view;
oldView && (delete this.children[oldView.cid]);
oldView && oldView._shouldDestroyOnNextSetView && oldView.destroy();
this._view && this._view.trigger('ready', options);
this.trigger('change:view:end', view, oldView, options);
return view;
},
getView: function() {
return this._view;
}
});
Handlebars.registerHelper('layout', function(options) {
options.hash[layoutCidAttributeName] = this._view.cid;
return new Handlebars.SafeString(Thorax.Util.tag.call(this, options.hash, '', this));
});
function ensureLayoutCid() {
++this._renderCount;
//set the layoutCidAttributeName on this.$el if there was no template
this.$el.attr(layoutCidAttributeName, this.cid);
}
function ensureLayoutViewsTargetElement() {
if (!this.$('[' + layoutCidAttributeName + '="' + this.cid + '"]')[0]) {
throw new Error('No layout element found in ' + (this.name || this.cid));
}
}
function getLayoutViewsTargetElement() {
return this.$('[' + layoutCidAttributeName + '="' + this.cid + '"]')[0] || this.el[0] || this.el;
}
//ViewController
Thorax.ViewController = Thorax.LayoutView.extend({
constructor: function() {
var response = Thorax.ViewController.__super__.constructor.apply(this, arguments);
this._bindRoutes();
initializeRouter.call(this);
//set the ViewController as the view on the parent
//if a parent was specified
this.on('route:before', function(router, name) {
if (this.parent && this.parent.getView) {
if (this.parent.getView() !== this) {
this.parent.setView(this, {
destroy: false
});
}
}
}, this);
return response;
}
});
_.extend(Thorax.ViewController.prototype, Thorax.Router.prototype);
var loadStart = 'load:start',
loadEnd = 'load:end',
rootObject;
Thorax.setRootObject = function(obj) {
rootObject = obj;
};
Thorax.loadHandler = function(start, end) {
return function(message, background, object) {
var self = this;
function startLoadTimeout() {
clearTimeout(self._loadStart.timeout);
self._loadStart.timeout = setTimeout(function() {
self._loadStart.run = true;
start.call(self, self._loadStart.message, self._loadStart.background, self._loadStart);
},
loadingTimeout*1000);
}
if (!self._loadStart) {
var loadingTimeout = self._loadingTimeoutDuration;
if (loadingTimeout === void 0) {
// If we are running on a non-view object pull the default timeout
loadingTimeout = Thorax.View.prototype._loadingTimeoutDuration;
}
self._loadStart = _.extend({
events: [],
timeout: 0,
message: message,
background: !!background
}, Backbone.Events);
startLoadTimeout();
} else {
clearTimeout(self._loadStart.endTimeout);
self._loadStart.message = message;
if (!background && self._loadStart.background) {
self._loadStart.background = false;
startLoadTimeout();
}
}
self._loadStart.events.push(object);
object.bind(loadEnd, function endCallback() {
object.off(loadEnd, endCallback);
var loadingEndTimeout = self._loadingTimeoutEndDuration;
if (loadingEndTimeout === void 0) {
// If we are running on a non-view object pull the default timeout
loadingEndTimeout = Thorax.View.prototype._loadingTimeoutEndDuration;
}
var events = self._loadStart.events,
index = events.indexOf(object);
if (index >= 0) {
events.splice(index, 1);
}
if (!events.length) {
self._loadStart.endTimeout = setTimeout(function(){
if (!events.length) {
var run = self._loadStart.run;
if (run) {
// Emit the end behavior, but only if there is a paired start
end.call(self, self._loadStart.background, self._loadStart);
self._loadStart.trigger(loadEnd, self._loadStart);
}
// If stopping make sure we don't run a start
clearTimeout(self._loadStart.timeout);
self._loadStart = undefined;
}
}, loadingEndTimeout * 1000);
}
});
};
};
/**
* Helper method for propagating load:start events to other objects.
*
* Forwards load:start events that occur on `source` to `dest`.
*/
Thorax.forwardLoadEvents = function(source, dest, once) {
function load(message, backgound, object) {
if (once) {
source.off(loadStart, load);
}
dest.trigger(loadStart, message, backgound, object);
}
source.on(loadStart, load);
return {
off: function() {
source.off(loadStart, load);
}
};
};
//
// Data load event generation
//
/**
* Mixing for generating load:start and load:end events.
*/
Thorax.mixinLoadable = function(target, useParent) {
_.extend(target, {
//loading config
_loadingClassName: 'loading',
_loadingTimeoutDuration: 0.33,
_loadingTimeoutEndDuration: 0.10,
// Propagates loading view parameters to the AJAX layer
onLoadStart: function(message, background, object) {
var that = useParent ? this.parent : this;
if (!that.nonBlockingLoad && !background && rootObject) {
rootObject.trigger(loadStart, message, background, object);
}
$(that.el).addClass(that._loadingClassName);
//used by loading helpers
if (that._loadingCallbacks) {
that._loadingCallbacks.forEach(function(callback) {
callback();
});
}
},
onLoadEnd: function(background, object) {
var that = useParent ? this.parent : this;
$(that.el).removeClass(that._loadingClassName);
//used by loading helpers
if (that._loadingCallbacks) {
that._loadingCallbacks.forEach(function(callback) {
callback();
});
}
}
});
};
Thorax.mixinLoadableEvents = function(target, useParent) {
_.extend(target, {
loadStart: function(message, background) {
var that = useParent ? this.parent : this;
that.trigger(loadStart, message, background, that);
},
loadEnd: function() {
var that = useParent ? this.parent : this;
that.trigger(loadEnd, that);
}
});
};
Thorax.mixinLoadable(Thorax.View.prototype);
Thorax.mixinLoadableEvents(Thorax.View.prototype);
Thorax.sync = function(method, dataObj, options) {
var self = this,
complete = options.complete;
options.complete = function() {
self._request = undefined;
self._aborted = false;
complete && complete.apply(this, arguments);
};
this._request = Backbone.sync.apply(this, arguments);
// TODO : Reevaluate this event... Seems too indepth to expose as an API
this.trigger('request', this._request);
return this._request;
};
function bindToRoute(callback, failback) {
var fragment = Backbone.history.getFragment(),
completed;
function finalizer(isCanceled) {
var same = fragment === Backbone.history.getFragment();
if (completed) {
// Prevent multiple execution, i.e. we were canceled but the success callback still runs
return;
}
if (isCanceled && same) {
// Ignore the first route event if we are running in newer versions of backbone
// where the route operation is a postfix operation.
return;
}
completed = true;
Backbone.history.off('route', resetLoader);
var args = Array.prototype.slice.call(arguments, 1);
if (!isCanceled && same) {
callback.apply(this, args);
} else {
failback && failback.apply(this, args);
}
}
var resetLoader = _.bind(finalizer, this, true);
Backbone.history.on('route', resetLoader);
return _.bind(finalizer, this, false);
}
function loadData(callback, failback, options) {
if (this.isPopulated()) {
return callback(this);
}
if (arguments.length === 2 && typeof failback !== 'function' && _.isObject(failback)) {
options = failback;
failback = false;
}
this.fetch(_.defaults({
success: bindToRoute(callback, failback && _.bind(failback, this, false)),
error: failback && _.bind(failback, this, true)
}, options));
}
function fetchQueue(options, $super) {
if (options.resetQueue) {
// WARN: Should ensure that loaders are protected from out of band data
// when using this option
this.fetchQueue = undefined;
}
if (!this.fetchQueue) {
// Kick off the request
this.fetchQueue = [options];
options = _.defaults({
success: flushQueue(this, this.fetchQueue, 'success'),
error: flushQueue(this, this.fetchQueue, 'error'),
complete: flushQueue(this, this.fetchQueue, 'complete')
}, options);
$super.call(this, options);
} else {
// Currently fetching. Queue and process once complete
this.fetchQueue.push(options);
}
}
function flushQueue(self, fetchQueue, handler) {
return function() {
var args = arguments;
// Flush the queue. Executes any callback handlers that
// may have been passed in the fetch options.
fetchQueue.forEach(function(options) {
if (options[handler]) {
options[handler].apply(this, args);
}
}, this);
// Reset the queue if we are still the active request
if (self.fetchQueue === fetchQueue) {
self.fetchQueue = undefined;
}
}
}
var klasses = [];
Thorax.Model && klasses.push(Thorax.Model);
Thorax.Collection && klasses.push(Thorax.Collection);
_.each(klasses, function(DataClass) {
var $fetch = DataClass.prototype.fetch;
Thorax.mixinLoadableEvents(DataClass.prototype, false);
_.extend(DataClass.prototype, {
sync: Thorax.sync,
fetch: function(options) {
options = options || {};
var self = this,
complete = options.complete;
options.complete = function() {
complete && complete.apply(this, arguments);
self.loadEnd();
};
self.loadStart(undefined, options.background);
return fetchQueue.call(this, options || {}, $fetch);
},
load: function(callback, failback, options) {
if (arguments.length === 2 && typeof failback !== 'function') {
options = failback;
failback = false;
}
options = options || {};
if (!options.background && !this.isPopulated() && rootObject) {
// Make sure that the global scope sees the proper load events here
// if we are loading in standalone mode
Thorax.forwardLoadEvents(this, rootObject, true);
}
var self = this;
loadData.call(this, callback,
function(isError) {
// Route changed, kill it
if (!isError) {
if (self._request) {
self._aborted = true;
self._request.abort();
}
}
failback && failback.apply && failback.apply(this, arguments);
},
options);
}
});
});
Thorax.Util.bindToRoute = bindToRoute;
if (Thorax.Router) {
Thorax.Router.bindToRoute = Thorax.Router.prototype.bindToRoute = bindToRoute;
}
//
// View load event handling
//
if (Thorax.Model) {
(function() {
// Propagates loading view parameters to the AJAX layer
var _setModelOptions = Thorax.View.prototype._setModelOptions;
Thorax.View.prototype._setModelOptions = function(options) {
return _setModelOptions.call(this, _.defaults({
ignoreErrors: this.ignoreFetchError,
background: this.nonBlockingLoad
}, options || {}));
};
})();
Thorax.View.prototype._loadModel = function(model, options) {
if (model.load) {
model.load(function() {
options.success && options.success(model);
}, options);
} else {
model.fetch(options);
}
};
}
if (Thorax.Collection) {
Thorax.mixinLoadable(Thorax.CollectionView.prototype);
Thorax.mixinLoadableEvents(Thorax.CollectionView.prototype);
// Propagates loading view parameters to the AJAX layer
var _setCollectionOptions = Thorax.CollectionView.prototype._setCollectionOptions;
Thorax.CollectionView.prototype._setCollectionOptions = function(collection, options) {
return _setCollectionOptions.call(this, collection, _.defaults({
ignoreErrors: this.ignoreFetchError,
background: this.nonBlockingLoad
}, options || {}));
};
Thorax.CollectionView.prototype._loadCollection = function(collection, options) {
if (collection.load) {
collection.load(function(){
options.success && options.success(collection);
}, options);
} else {
collection.fetch(options);
}
};
}
Thorax.View.on({
'load:start': Thorax.loadHandler(
function(message, background, object) {
this.onLoadStart(message, background, object);
},
function(background, object) {
this.onLoadEnd(object);
}),
collection: {
'load:start': function(collectionView, message, background, object) {
//this refers to the collection view, we want to trigger on
//the parent view which originally bound the collection
this.trigger(loadStart, message, background, object);
}
},
model: {
'load:start': function(message, background, object) {
this.trigger(loadStart, message, background, object);
}
}
});
// Helpers
Handlebars.registerViewHelper('loading', function(view) {
_render = view.render;
view.render = function() {
if (view.parent.$el.hasClass(view.parent._loadingClassName)) {
return _render.call(this, view.fn);
} else {
return _render.call(this, view.inverse);
}
};
var callback = _.bind(view.render, view);
view.parent._loadingCallbacks = view.parent._loadingCallbacks || [];
view.parent._loadingCallbacks.push(callback);
view.on('freeze', function() {
view.parent._loadingCallbacks = _.without(view.parent._loadingCallbacks, callback);
});
view.render();
});
//add "loading-view" and "loading-template" options to collection helper
Thorax.View.on('helper:collection', function(view) {
if (arguments.length === 2) {
view = arguments[1];
}
if (!view.collection) {
view.collection = view.parent.collection;
}
if (view.options['loading-view'] || view.options['loading-template']) {
var item;
var callback = Thorax.loadHandler(_.bind(function() {
if (view.collection.length === 0) {
view.$el.empty();
}
if (view.options['loading-view']) {
var instance = Thorax.Util.getViewInstance(view.options['loading-view'], {
collection: view.collection
});
view._addChild(instance);
if (view.options['loading-template']) {
instance.render(view.options['loading-template']);
} else {
instance.render();
}
item = instance;
} else {
item = view.renderTemplate(view.options['loading-template'], {
collection: view.collection
});
}
view.appendItem(item, view.collection.length);
view.$el.children().last().attr('data-loading-element', view.collection.cid);
}, this), _.bind(function() {
view.$el.find('[data-loading-element="' + view.collection.cid + '"]').remove();
}, this));
view.on(view.collection, 'load:start', callback);
}
});
if (Thorax.CollectionView) {
Thorax.CollectionView._optionNames.push('loading-template');
Thorax.CollectionView._optionNames.push('loading-view');
}
})();
// Underscore.js 1.3.3
// (c) 2009-2012 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
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
var app = app || {};
(function() {
'use strict';
// Todo Model
// ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
app.Todo = Backbone.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
defaults: {
title: '',
completed: false
},
// Toggle the `completed` state of this todo item.
toggle: function() {
this.save({
completed: !this.get('completed')
});
}
});
}());
var app = app || {};
(function() {
'use strict';
// Todo Router
// ----------
app.TodoRouter = new (Thorax.Router.extend({
routes: {
'*filter': 'setFilter'
},
setFilter: function( param ) {
// Set the current filter to be used
window.app.TodoFilter = param.trim() || '';
// Trigger a collection filter event, causing hiding/unhiding
// of Todo view items
window.app.Todos.trigger('filter');
}
}));
}());
var app = app || {};
$(function( $ ) {
'use strict';
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
Thorax.View.extend({
// This will assign the template Thorax.templates['app'] to the view and
// create a view class at Thorax.Views['app']
name: 'app',
// Delegated events for creating new items, and clearing completed ones.
events: {
'keypress #new-todo': 'createOnEnter',
'click #toggle-all': 'toggleAllComplete',
// The collection helper in the template will bind the collection
// to the view. Any events in this hash will be bound to the
// collection.
collection: {
all: 'toggleToggleAllButton',
'change:completed': 'filterOne',
'filter': 'filterAll'
},
rendered: 'toggleToggleAllButton'
},
// Unless the "context" method is overriden any attributes on the view
// will be availble to the context / scope of the template, make the
// global Todos collection available to the template.
// Load any preexisting todos that might be saved in *localStorage*.
initialize: function() {
this.todosCollection = app.Todos;
this.todosCollection.fetch();
this.render();
},
toggleToggleAllButton: function() {
this.$('#toggle-all').attr('checked', !this.todosCollection.remaining().length);
},
filterOne : function (todo) {
todo.trigger("visible");
},
filterAll : function () {
app.Todos.each(this.filterOne, this);
},
// Generate the attributes for a new Todo item.
newAttributes: function() {
return {
title: this.$('#new-todo').val().trim(),
order: app.Todos.nextOrder(),
completed: false
};
},
// If you hit return in the main input field, create new **Todo** model,
// persisting it to *localStorage*.
createOnEnter: function( e ) {
if ( e.which !== ENTER_KEY || !this.$('#new-todo').val().trim() ) {
return;
}
app.Todos.create( this.newAttributes() );
this.$('#new-todo').val('');
},
toggleAllComplete: function() {
var completed = this.$('#toggle-all')[0].checked;
app.Todos.each(function( todo ) {
todo.save({
'completed': completed
});
});
}
});
});
Thorax.View.extend({
name: 'stats',
events: {
'click #clear-completed': 'clearCompleted',
// The "rendered" event is triggered by Thorax each time render()
// is called and the result of the template has been appended
// to the View's $el
rendered: 'highlightFilter'
},
initialize: function() {
// Whenever the Todos collection changes re-render the stats
// render() needs to be called with no arguments, otherwise calling
// it with arguments will insert the arguments as content
window.app.Todos.on('all', _.debounce(function() {
this.render();
}), this);
},
// Clear all completed todo items, destroying their models.
clearCompleted: function() {
_.each( window.app.Todos.completed(), function( todo ) {
todo.destroy();
});
return false;
},
// Each time the stats view is rendered this function will
// be called to generate the context / scope that the template
// will be called with. "context" defaults to "return this"
context: function() {
return {
completed: app.Todos.completed().length,
remaining: app.Todos.remaining().length
};
},
// Highlight which filter will appear to be active
highlightFilter: function() {
this.$('#filters li a')
.removeClass('selected')
.filter('[href="#/' + ( app.TodoFilter || '' ) + '"]')
.addClass('selected');
}
});
\ No newline at end of file
var app = app || {};
$(function() {
'use strict';
// Todo Item View
// --------------
// The DOM element for a todo item...
Thorax.View.extend({
//... is a list tag.
tagName: 'li',
// Cache the template function for a single item.
name: 'todo-item',
// The DOM events specific to an item.
events: {
'click .toggle': 'toggleCompleted',
'dblclick label': 'edit',
'click .destroy': 'clear',
'keypress .edit': 'updateOnEnter',
'blur .edit': 'close',
// Events in this hash will be bound to the model
// when it is assigned to the view
model: {
visible: 'toggleVisible'
},
// The "rendered" event is triggered by Thorax each time render()
// is called and the result of the template has been appended
// to the View's $el
rendered: function() {
this.$el.toggleClass( 'completed', this.model.get('completed') );
this.toggleVisible();
this.input = this.$('.edit');
}
},
toggleVisible : function () {
this.$el.toggleClass( 'hidden', this.isHidden());
},
isHidden : function () {
var isCompleted = this.model.get('completed');
return ( // hidden cases only
(!isCompleted && app.TodoFilter === 'completed')
|| (isCompleted && app.TodoFilter === 'active')
);
},
// Toggle the `"completed"` state of the model.
toggleCompleted: function() {
this.model.toggle();
},
// Switch this view into `"editing"` mode, displaying the input field.
edit: function() {
this.$el.addClass('editing');
this.input.focus();
},
// Close the `"editing"` mode, saving changes to the todo.
close: function() {
var value = this.input.val().trim();
if ( value ) {
this.model.save({ title: value });
} else {
this.clear();
}
this.$el.removeClass('editing');
},
// If you hit `enter`, we're through editing the item.
updateOnEnter: function( e ) {
if ( e.which === ENTER_KEY ) {
this.close();
}
},
// Remove the item, destroy the model from *localStorage* and delete its view.
clear: function() {
this.model.destroy();
}
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment