├── .gitignore ├── public ├── stylesheets │ └── style.less └── js │ ├── jquery.tmpl.min.js │ ├── underscore-1.1.0.js │ ├── backbone.js │ └── jquery-1.4.4.min.js ├── test └── app.test.js ├── views ├── layout.ejs ├── index.ejs └── examples │ ├── simple-list.ejs │ ├── click-counter.ejs │ └── hello-world.ejs ├── app.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | .DS_Store 4 | *.swo 5 | *.swp 6 | .*.swp 7 | .*.swo 8 | .*.bak 9 | !.gitignore 10 | -------------------------------------------------------------------------------- /public/stylesheets/style.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } -------------------------------------------------------------------------------- /test/app.test.js: -------------------------------------------------------------------------------- 1 | 2 | // Run $ expresso 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | var app = require('../app'); 9 | 10 | 11 | module.exports = { 12 | 'GET /': function(assert){ 13 | assert.response(app, 14 | { url: '/' }, 15 | { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' }}, 16 | function(res){ 17 | assert.includes(res.body, 'Express'); 18 | }); 19 | } 20 | }; -------------------------------------------------------------------------------- /views/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | <%- body %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 |

This project contains Knockout examples ported to Backbone. My main motivation 2 | is to learn enough about each to determine which framework best suits my 3 | style.

4 | 5 |

My initial impression is Knockout is the more elegant framework. However, 6 | everything the author of Backbone has done has been pretty darn good so 7 | there is likely a good reason why Backbone appears more complicated.

8 | 9 | 14 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express'); 7 | var app = module.exports = express.createServer(); 8 | 9 | 10 | // Configuration 11 | 12 | app.configure(function(){ 13 | app.set('views', __dirname + '/views'); 14 | app.set('view engine', 'ejs'); 15 | app.use(express.bodyDecoder()); 16 | app.use(express.methodOverride()); 17 | app.use(express.compiler({ src: __dirname + '/public', enable: ['less'] })); 18 | app.use(app.router); 19 | app.use(express.staticProvider(__dirname + '/public')); 20 | }); 21 | 22 | app.configure('development', function(){ 23 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 24 | }); 25 | 26 | app.configure('production', function(){ 27 | app.use(express.errorHandler()); 28 | }); 29 | 30 | 31 | // Routes 32 | 33 | 34 | app.get('/:example', function(req, res){ 35 | res.render('examples/' + req.params.example, { 36 | locals: { 37 | title: req.params.example 38 | } 39 | }); 40 | }); 41 | 42 | app.get('/', function(req, res){ 43 | res.render('index', { 44 | locals: { 45 | title: 'Backbone Examples Ported From Knockout' 46 | } 47 | }); 48 | }); 49 | 50 | 51 | // Only listen on $ node app.js 52 | 53 | if (!module.parent) { 54 | app.listen(3000); 55 | console.log("Express server listening on port %d", app.address().port); 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backbone Examples from Knockout 2 | 3 | This project contains [Knockout](http://knockoutjs.com) examples ported to 4 | [Backbone](https://documentcloud.github.com/backbone/). The motivation 5 | is to learn enough about each to determine which framework best suits my 6 | style. 7 | 8 | ## Opinion 9 | 10 | My initial impression is Knockout is the more elegant 11 | framework as of this writing. However, almost everything [jashkenas](https://github.com/jashkenas), 12 | the author of Backbone, has created has been excellent. Backbone's 13 | markup is cleaner, which facilitates integrating creative 14 | assets from designers. Backbone's' synchronization with RESTful services 15 | could also be a plus. We'll see. 16 | 17 | Knockout's examples have too much inline javascript in data attributes. Perhaps that 18 | is intentional to keep the examples concise. Not sure I like that. Who knows, I'm un-learning 19 | a lot of things and that may be one of those compromises which makes code simpler at 20 | the expense of *architectural* correctness. 21 | 22 | ## Pre-requisites 23 | 24 | * [express](https://github.com/visionmedia/express) - awesome web framework 25 | * [ejs](https://github.com/kof/node-jqtpl) 26 | 27 | Install both via [npm](https://github.com/isaacs/npm) 28 | 29 | ## Examples Ported 30 | 31 | * Hello World 32 | * Click Counter 33 | * Simple List 34 | 35 | ## Run It 36 | 37 | node app.js 38 | 39 | ## TODOS 40 | 41 | * Use Docco 42 | * Create a Pretty Examples Site 43 | -------------------------------------------------------------------------------- /views/examples/simple-list.ejs: -------------------------------------------------------------------------------- 1 |

Simple List Example

2 | 3 |

4 | Knockout's Example 5 |

6 | 7 |
8 | New item: 9 | 10 | 11 |

Your items:

12 | 13 |
14 | 15 | 60 | -------------------------------------------------------------------------------- /views/examples/click-counter.ejs: -------------------------------------------------------------------------------- 1 |

Click Counter Example

2 | 3 |

4 | Knockout's Example 5 |

6 | 7 |
You've clicked   times
8 | 9 | 10 | 11 |
12 | That's too many clicks! Please stop before you wear out your fingers. 13 | 14 |
15 | 16 | 67 | -------------------------------------------------------------------------------- /views/examples/hello-world.ejs: -------------------------------------------------------------------------------- 1 |

Hello World Example

2 | 3 | 7 |

First name:

8 |

Last name:

9 |

Hello, !

10 | 11 | 59 | -------------------------------------------------------------------------------- /public/js/jquery.tmpl.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},h=0,c=0,l=[];function g(e,d,g,i){var c={data:i||(d?d.data:{}),_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};e&&a.extend(c,e,{nodes:[],parent:d});if(g){c.tmpl=g;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++h;(l.length?f:b)[h]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h0?this.clone(true):this).get();a.fn[d].apply(a(i[h]),k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,l,j){if(d[0]&&d[0].nodeType){var f=a.makeArray(arguments),g=d.length,i=0,h;while(i1)f[0]=[a.makeArray(d)];if(h&&c)f[2]=function(b){a.tmpl.afterManip(this,b,j)};r.apply(this,f)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var j,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(i(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);j=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(i(c,null,j)):j},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){_=_.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(_,$1,$2);_=[];",close:"call=$item.calls();_=call._.concat($item.wrap(call,_));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){_.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){_.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function i(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:i(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=j(c).concat(b);if(d)b=b.concat(j(d))});return b?b:j(c)}function j(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,_=[],$data=$item.data;with($data){_.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,j,d,b,c,e){var i=a.tmpl.tag[j],h,f,g;if(!i)throw"Template command not found: "+j;h=i._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=k(b);e=e?","+k(e)+")":c?")":"";f=c?b.indexOf(".")>-1?b+c:"("+b+").call($item"+e:b;g=c?f:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else g=f=h.$1||"null";d=k(d);return"');"+i[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(g).split("$1").join(f).split("$2").join(d?d.replace(/\s*([^\(]+)\s*(\((.*?)\))?/g,function(d,c,b,a){a=a?","+a+")":b?")":"";return a?"("+c+").call($item"+a:d}):h.$2||"")+"_.push('"})+"');}return _;")}function n(c,b){c._wrap=i(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function k(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,i;for(e=0,p=o.length;e=0;i--)m(j[i]);m(k)}function m(j){var p,i=j,k,e,m;if(m=j.getAttribute(d)){while(i.parentNode&&(i=i.parentNode).nodeType===1&&!(p=i.getAttribute(d)));if(p!==m){i=i.parentNode?i.nodeType===11?0:i.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[i]||f[i],null,true);e.key=++h;b[h]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;i=a.data(j.parentNode,"tmplItem");i=i?i.key:0}if(e){k=e;while(k&&k.key!=i){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent,null,true)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery) -------------------------------------------------------------------------------- /public/js/underscore-1.1.0.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the terms of the MIT license. 4 | // Portions of Underscore are inspired by or borrowed from Prototype.js, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | 9 | (function() { 10 | // ------------------------- Baseline setup --------------------------------- 11 | 12 | // Establish the root object, "window" in the browser, or "global" on the server. 13 | var root = this; 14 | 15 | // Save the previous value of the "_" variable. 16 | var previousUnderscore = root._; 17 | 18 | // Establish the object that gets thrown to break out of a loop iteration. 19 | var breaker = typeof StopIteration !== 'undefined' ? StopIteration : '__break__'; 20 | 21 | // Quick regexp-escaping function, because JS doesn't have RegExp.escape(). 22 | var escapeRegExp = function(s) { return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); }; 23 | 24 | // Save bytes in the minified (but not gzipped) version: 25 | var ArrayProto = Array.prototype, ObjProto = Object.prototype; 26 | 27 | // Create quick reference variables for speed access to core prototypes. 28 | var slice = ArrayProto.slice, 29 | unshift = ArrayProto.unshift, 30 | toString = ObjProto.toString, 31 | hasOwnProperty = ObjProto.hasOwnProperty, 32 | propertyIsEnumerable = ObjProto.propertyIsEnumerable; 33 | 34 | // All ECMA5 native implementations we hope to use are declared here. 35 | var 36 | nativeForEach = ArrayProto.forEach, 37 | nativeMap = ArrayProto.map, 38 | nativeReduce = ArrayProto.reduce, 39 | nativeReduceRight = ArrayProto.reduceRight, 40 | nativeFilter = ArrayProto.filter, 41 | nativeEvery = ArrayProto.every, 42 | nativeSome = ArrayProto.some, 43 | nativeIndexOf = ArrayProto.indexOf, 44 | nativeLastIndexOf = ArrayProto.lastIndexOf, 45 | nativeIsArray = Array.isArray, 46 | nativeKeys = Object.keys; 47 | 48 | // Create a safe reference to the Underscore object for use below. 49 | var _ = function(obj) { return new wrapper(obj); }; 50 | 51 | // Export the Underscore object for CommonJS. 52 | if (typeof exports !== 'undefined') exports._ = _; 53 | 54 | // Export underscore to global scope. 55 | root._ = _; 56 | 57 | // Current version. 58 | _.VERSION = '1.1.0'; 59 | 60 | // ------------------------ Collection Functions: --------------------------- 61 | 62 | // The cornerstone, an each implementation. 63 | // Handles objects implementing forEach, arrays, and raw objects. 64 | // Delegates to JavaScript 1.6's native forEach if available. 65 | var each = _.forEach = function(obj, iterator, context) { 66 | try { 67 | if (nativeForEach && obj.forEach === nativeForEach) { 68 | obj.forEach(iterator, context); 69 | } else if (_.isNumber(obj.length)) { 70 | for (var i = 0, l = obj.length; i < l; i++) iterator.call(context, obj[i], i, obj); 71 | } else { 72 | for (var key in obj) { 73 | if (hasOwnProperty.call(obj, key)) iterator.call(context, obj[key], key, obj); 74 | } 75 | } 76 | } catch(e) { 77 | if (e != breaker) throw e; 78 | } 79 | return obj; 80 | }; 81 | 82 | // Return the results of applying the iterator to each element. 83 | // Delegates to JavaScript 1.6's native map if available. 84 | _.map = function(obj, iterator, context) { 85 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 86 | var results = []; 87 | each(obj, function(value, index, list) { 88 | results.push(iterator.call(context, value, index, list)); 89 | }); 90 | return results; 91 | }; 92 | 93 | // Reduce builds up a single result from a list of values, aka inject, or foldl. 94 | // Delegates to JavaScript 1.8's native reduce if available. 95 | _.reduce = function(obj, iterator, memo, context) { 96 | if (nativeReduce && obj.reduce === nativeReduce) { 97 | if (context) iterator = _.bind(iterator, context); 98 | return obj.reduce(iterator, memo); 99 | } 100 | each(obj, function(value, index, list) { 101 | memo = iterator.call(context, memo, value, index, list); 102 | }); 103 | return memo; 104 | }; 105 | 106 | // The right-associative version of reduce, also known as foldr. Uses 107 | // Delegates to JavaScript 1.8's native reduceRight if available. 108 | _.reduceRight = function(obj, iterator, memo, context) { 109 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 110 | if (context) iterator = _.bind(iterator, context); 111 | return obj.reduceRight(iterator, memo); 112 | } 113 | var reversed = _.clone(_.toArray(obj)).reverse(); 114 | return _.reduce(reversed, iterator, memo, context); 115 | }; 116 | 117 | // Return the first value which passes a truth test. 118 | _.detect = function(obj, iterator, context) { 119 | var result; 120 | each(obj, function(value, index, list) { 121 | if (iterator.call(context, value, index, list)) { 122 | result = value; 123 | _.breakLoop(); 124 | } 125 | }); 126 | return result; 127 | }; 128 | 129 | // Return all the elements that pass a truth test. 130 | // Delegates to JavaScript 1.6's native filter if available. 131 | _.filter = function(obj, iterator, context) { 132 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 133 | var results = []; 134 | each(obj, function(value, index, list) { 135 | iterator.call(context, value, index, list) && results.push(value); 136 | }); 137 | return results; 138 | }; 139 | 140 | // Return all the elements for which a truth test fails. 141 | _.reject = function(obj, iterator, context) { 142 | var results = []; 143 | each(obj, function(value, index, list) { 144 | !iterator.call(context, value, index, list) && results.push(value); 145 | }); 146 | return results; 147 | }; 148 | 149 | // Determine whether all of the elements match a truth test. 150 | // Delegates to JavaScript 1.6's native every if available. 151 | _.every = function(obj, iterator, context) { 152 | iterator = iterator || _.identity; 153 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 154 | var result = true; 155 | each(obj, function(value, index, list) { 156 | if (!(result = result && iterator.call(context, value, index, list))) _.breakLoop(); 157 | }); 158 | return result; 159 | }; 160 | 161 | // Determine if at least one element in the object matches a truth test. 162 | // Delegates to JavaScript 1.6's native some if available. 163 | _.some = function(obj, iterator, context) { 164 | iterator = iterator || _.identity; 165 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 166 | var result = false; 167 | each(obj, function(value, index, list) { 168 | if (result = iterator.call(context, value, index, list)) _.breakLoop(); 169 | }); 170 | return result; 171 | }; 172 | 173 | // Determine if a given value is included in the array or object using '==='. 174 | _.include = function(obj, target) { 175 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 176 | var found = false; 177 | each(obj, function(value) { 178 | if (found = value === target) _.breakLoop(); 179 | }); 180 | return found; 181 | }; 182 | 183 | // Invoke a method with arguments on every item in a collection. 184 | _.invoke = function(obj, method) { 185 | var args = _.rest(arguments, 2); 186 | return _.map(obj, function(value) { 187 | return (method ? value[method] : value).apply(value, args); 188 | }); 189 | }; 190 | 191 | // Convenience version of a common use case of map: fetching a property. 192 | _.pluck = function(obj, key) { 193 | return _.map(obj, function(value){ return value[key]; }); 194 | }; 195 | 196 | // Return the maximum item or (item-based computation). 197 | _.max = function(obj, iterator, context) { 198 | if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); 199 | var result = {computed : -Infinity}; 200 | each(obj, function(value, index, list) { 201 | var computed = iterator ? iterator.call(context, value, index, list) : value; 202 | computed >= result.computed && (result = {value : value, computed : computed}); 203 | }); 204 | return result.value; 205 | }; 206 | 207 | // Return the minimum element (or element-based computation). 208 | _.min = function(obj, iterator, context) { 209 | if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); 210 | var result = {computed : Infinity}; 211 | each(obj, function(value, index, list) { 212 | var computed = iterator ? iterator.call(context, value, index, list) : value; 213 | computed < result.computed && (result = {value : value, computed : computed}); 214 | }); 215 | return result.value; 216 | }; 217 | 218 | // Sort the object's values by a criterion produced by an iterator. 219 | _.sortBy = function(obj, iterator, context) { 220 | return _.pluck(_.map(obj, function(value, index, list) { 221 | return { 222 | value : value, 223 | criteria : iterator.call(context, value, index, list) 224 | }; 225 | }).sort(function(left, right) { 226 | var a = left.criteria, b = right.criteria; 227 | return a < b ? -1 : a > b ? 1 : 0; 228 | }), 'value'); 229 | }; 230 | 231 | // Use a comparator function to figure out at what index an object should 232 | // be inserted so as to maintain order. Uses binary search. 233 | _.sortedIndex = function(array, obj, iterator) { 234 | iterator = iterator || _.identity; 235 | var low = 0, high = array.length; 236 | while (low < high) { 237 | var mid = (low + high) >> 1; 238 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 239 | } 240 | return low; 241 | }; 242 | 243 | // Convert anything iterable into a real, live array. 244 | _.toArray = function(iterable) { 245 | if (!iterable) return []; 246 | if (iterable.toArray) return iterable.toArray(); 247 | if (_.isArray(iterable)) return iterable; 248 | if (_.isArguments(iterable)) return slice.call(iterable); 249 | return _.values(iterable); 250 | }; 251 | 252 | // Return the number of elements in an object. 253 | _.size = function(obj) { 254 | return _.toArray(obj).length; 255 | }; 256 | 257 | // -------------------------- Array Functions: ------------------------------ 258 | 259 | // Get the first element of an array. Passing "n" will return the first N 260 | // values in the array. Aliased as "head". The "guard" check allows it to work 261 | // with _.map. 262 | _.first = function(array, n, guard) { 263 | return n && !guard ? slice.call(array, 0, n) : array[0]; 264 | }; 265 | 266 | // Returns everything but the first entry of the array. Aliased as "tail". 267 | // Especially useful on the arguments object. Passing an "index" will return 268 | // the rest of the values in the array from that index onward. The "guard" 269 | //check allows it to work with _.map. 270 | _.rest = function(array, index, guard) { 271 | return slice.call(array, _.isUndefined(index) || guard ? 1 : index); 272 | }; 273 | 274 | // Get the last element of an array. 275 | _.last = function(array) { 276 | return array[array.length - 1]; 277 | }; 278 | 279 | // Trim out all falsy values from an array. 280 | _.compact = function(array) { 281 | return _.filter(array, function(value){ return !!value; }); 282 | }; 283 | 284 | // Return a completely flattened version of an array. 285 | _.flatten = function(array) { 286 | return _.reduce(array, function(memo, value) { 287 | if (_.isArray(value)) return memo.concat(_.flatten(value)); 288 | memo.push(value); 289 | return memo; 290 | }, []); 291 | }; 292 | 293 | // Return a version of the array that does not contain the specified value(s). 294 | _.without = function(array) { 295 | var values = _.rest(arguments); 296 | return _.filter(array, function(value){ return !_.include(values, value); }); 297 | }; 298 | 299 | // Produce a duplicate-free version of the array. If the array has already 300 | // been sorted, you have the option of using a faster algorithm. 301 | _.uniq = function(array, isSorted) { 302 | return _.reduce(array, function(memo, el, i) { 303 | if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo.push(el); 304 | return memo; 305 | }, []); 306 | }; 307 | 308 | // Produce an array that contains every item shared between all the 309 | // passed-in arrays. 310 | _.intersect = function(array) { 311 | var rest = _.rest(arguments); 312 | return _.filter(_.uniq(array), function(item) { 313 | return _.every(rest, function(other) { 314 | return _.indexOf(other, item) >= 0; 315 | }); 316 | }); 317 | }; 318 | 319 | // Zip together multiple lists into a single array -- elements that share 320 | // an index go together. 321 | _.zip = function() { 322 | var args = _.toArray(arguments); 323 | var length = _.max(_.pluck(args, 'length')); 324 | var results = new Array(length); 325 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, String(i)); 326 | return results; 327 | }; 328 | 329 | // If the browser doesn't supply us with indexOf (I'm looking at you, MSIE), 330 | // we need this function. Return the position of the first occurence of an 331 | // item in an array, or -1 if the item is not included in the array. 332 | // Delegates to JavaScript 1.8's native indexOf if available. 333 | _.indexOf = function(array, item) { 334 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 335 | for (var i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; 336 | return -1; 337 | }; 338 | 339 | 340 | // Delegates to JavaScript 1.6's native lastIndexOf if available. 341 | _.lastIndexOf = function(array, item) { 342 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 343 | var i = array.length; 344 | while (i--) if (array[i] === item) return i; 345 | return -1; 346 | }; 347 | 348 | // Generate an integer Array containing an arithmetic progression. A port of 349 | // the native Python range() function. See: 350 | // http://docs.python.org/library/functions.html#range 351 | _.range = function(start, stop, step) { 352 | var a = _.toArray(arguments); 353 | var solo = a.length <= 1; 354 | var start = solo ? 0 : a[0], stop = solo ? a[0] : a[1], step = a[2] || 1; 355 | var len = Math.ceil((stop - start) / step); 356 | if (len <= 0) return []; 357 | var range = new Array(len); 358 | for (var i = start, idx = 0; true; i += step) { 359 | if ((step > 0 ? i - stop : stop - i) >= 0) return range; 360 | range[idx++] = i; 361 | } 362 | }; 363 | 364 | // ----------------------- Function Functions: ------------------------------ 365 | 366 | // Create a function bound to a given object (assigning 'this', and arguments, 367 | // optionally). Binding with arguments is also known as 'curry'. 368 | _.bind = function(func, obj) { 369 | var args = _.rest(arguments, 2); 370 | return function() { 371 | return func.apply(obj || {}, args.concat(_.toArray(arguments))); 372 | }; 373 | }; 374 | 375 | // Bind all of an object's methods to that object. Useful for ensuring that 376 | // all callbacks defined on an object belong to it. 377 | _.bindAll = function(obj) { 378 | var funcs = _.rest(arguments); 379 | if (funcs.length == 0) funcs = _.functions(obj); 380 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 381 | return obj; 382 | }; 383 | 384 | // Memoize an expensive function by storing its results. 385 | _.memoize = function(func, hasher) { 386 | var memo = {}; 387 | hasher = hasher || _.identity; 388 | return function() { 389 | var key = hasher.apply(this, arguments); 390 | return key in memo ? memo[key] : (memo[key] = func.apply(this, arguments)); 391 | }; 392 | }; 393 | 394 | // Delays a function for the given number of milliseconds, and then calls 395 | // it with the arguments supplied. 396 | _.delay = function(func, wait) { 397 | var args = _.rest(arguments, 2); 398 | return setTimeout(function(){ return func.apply(func, args); }, wait); 399 | }; 400 | 401 | // Defers a function, scheduling it to run after the current call stack has 402 | // cleared. 403 | _.defer = function(func) { 404 | return _.delay.apply(_, [func, 1].concat(_.rest(arguments))); 405 | }; 406 | 407 | // Returns the first function passed as an argument to the second, 408 | // allowing you to adjust arguments, run code before and after, and 409 | // conditionally execute the original function. 410 | _.wrap = function(func, wrapper) { 411 | return function() { 412 | var args = [func].concat(_.toArray(arguments)); 413 | return wrapper.apply(wrapper, args); 414 | }; 415 | }; 416 | 417 | // Returns a function that is the composition of a list of functions, each 418 | // consuming the return value of the function that follows. 419 | _.compose = function() { 420 | var funcs = _.toArray(arguments); 421 | return function() { 422 | var args = _.toArray(arguments); 423 | for (var i=funcs.length-1; i >= 0; i--) { 424 | args = [funcs[i].apply(this, args)]; 425 | } 426 | return args[0]; 427 | }; 428 | }; 429 | 430 | // ------------------------- Object Functions: ------------------------------ 431 | 432 | // Retrieve the names of an object's properties. 433 | // Delegates to ECMA5's native Object.keys 434 | _.keys = nativeKeys || function(obj) { 435 | if (_.isArray(obj)) return _.range(0, obj.length); 436 | var keys = []; 437 | for (var key in obj) if (hasOwnProperty.call(obj, key)) keys.push(key); 438 | return keys; 439 | }; 440 | 441 | // Retrieve the values of an object's properties. 442 | _.values = function(obj) { 443 | return _.map(obj, _.identity); 444 | }; 445 | 446 | // Return a sorted list of the function names available on the object. 447 | _.functions = function(obj) { 448 | return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort(); 449 | }; 450 | 451 | // Extend a given object with all the properties in passed-in object(s). 452 | _.extend = function(obj) { 453 | each(_.rest(arguments), function(source) { 454 | for (var prop in source) obj[prop] = source[prop]; 455 | }); 456 | return obj; 457 | }; 458 | 459 | // Create a (shallow-cloned) duplicate of an object. 460 | _.clone = function(obj) { 461 | if (_.isArray(obj)) return obj.slice(0); 462 | return _.extend({}, obj); 463 | }; 464 | 465 | // Invokes interceptor with the obj, and then returns obj. 466 | // The primary purpose of this method is to "tap into" a method chain, in order to perform operations on intermediate results within the chain. 467 | _.tap = function(obj, interceptor) { 468 | interceptor(obj); 469 | return obj; 470 | }; 471 | 472 | // Perform a deep comparison to check if two objects are equal. 473 | _.isEqual = function(a, b) { 474 | // Check object identity. 475 | if (a === b) return true; 476 | // Different types? 477 | var atype = typeof(a), btype = typeof(b); 478 | if (atype != btype) return false; 479 | // Basic equality test (watch out for coercions). 480 | if (a == b) return true; 481 | // One is falsy and the other truthy. 482 | if ((!a && b) || (a && !b)) return false; 483 | // One of them implements an isEqual()? 484 | if (a.isEqual) return a.isEqual(b); 485 | // Check dates' integer values. 486 | if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime(); 487 | // Both are NaN? 488 | if (_.isNaN(a) && _.isNaN(b)) return false; 489 | // Compare regular expressions. 490 | if (_.isRegExp(a) && _.isRegExp(b)) 491 | return a.source === b.source && 492 | a.global === b.global && 493 | a.ignoreCase === b.ignoreCase && 494 | a.multiline === b.multiline; 495 | // If a is not an object by this point, we can't handle it. 496 | if (atype !== 'object') return false; 497 | // Check for different array lengths before comparing contents. 498 | if (a.length && (a.length !== b.length)) return false; 499 | // Nothing else worked, deep compare the contents. 500 | var aKeys = _.keys(a), bKeys = _.keys(b); 501 | // Different object sizes? 502 | if (aKeys.length != bKeys.length) return false; 503 | // Recursive comparison of contents. 504 | for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false; 505 | return true; 506 | }; 507 | 508 | // Is a given array or object empty? 509 | _.isEmpty = function(obj) { 510 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 511 | for (var key in obj) if (hasOwnProperty.call(obj, key)) return false; 512 | return true; 513 | }; 514 | 515 | // Is a given value a DOM element? 516 | _.isElement = function(obj) { 517 | return !!(obj && obj.nodeType == 1); 518 | }; 519 | 520 | // Is a given value an array? 521 | // Delegates to ECMA5's native Array.isArray 522 | _.isArray = nativeIsArray || function(obj) { 523 | return !!(obj && obj.concat && obj.unshift && !obj.callee); 524 | }; 525 | 526 | // Is a given variable an arguments object? 527 | _.isArguments = function(obj) { 528 | return obj && obj.callee; 529 | }; 530 | 531 | // Is a given value a function? 532 | _.isFunction = function(obj) { 533 | return !!(obj && obj.constructor && obj.call && obj.apply); 534 | }; 535 | 536 | // Is a given value a string? 537 | _.isString = function(obj) { 538 | return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); 539 | }; 540 | 541 | // Is a given value a number? 542 | _.isNumber = function(obj) { 543 | return (obj === +obj) || (toString.call(obj) === '[object Number]'); 544 | }; 545 | 546 | // Is a given value a boolean? 547 | _.isBoolean = function(obj) { 548 | return obj === true || obj === false; 549 | }; 550 | 551 | // Is a given value a date? 552 | _.isDate = function(obj) { 553 | return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear); 554 | }; 555 | 556 | // Is the given value a regular expression? 557 | _.isRegExp = function(obj) { 558 | return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false)); 559 | }; 560 | 561 | // Is the given value NaN -- this one is interesting. NaN != NaN, and 562 | // isNaN(undefined) == true, so we make sure it's a number first. 563 | _.isNaN = function(obj) { 564 | return _.isNumber(obj) && isNaN(obj); 565 | }; 566 | 567 | // Is a given value equal to null? 568 | _.isNull = function(obj) { 569 | return obj === null; 570 | }; 571 | 572 | // Is a given variable undefined? 573 | _.isUndefined = function(obj) { 574 | return typeof obj == 'undefined'; 575 | }; 576 | 577 | // -------------------------- Utility Functions: ---------------------------- 578 | 579 | // Run Underscore.js in noConflict mode, returning the '_' variable to its 580 | // previous owner. Returns a reference to the Underscore object. 581 | _.noConflict = function() { 582 | root._ = previousUnderscore; 583 | return this; 584 | }; 585 | 586 | // Keep the identity function around for default iterators. 587 | _.identity = function(value) { 588 | return value; 589 | }; 590 | 591 | // Run a function n times. 592 | _.times = function (n, iterator, context) { 593 | for (var i = 0; i < n; i++) iterator.call(context, i); 594 | }; 595 | 596 | // Break out of the middle of an iteration. 597 | _.breakLoop = function() { 598 | throw breaker; 599 | }; 600 | 601 | // Add your own custom functions to the Underscore object, ensuring that 602 | // they're correctly added to the OOP wrapper as well. 603 | _.mixin = function(obj) { 604 | each(_.functions(obj), function(name){ 605 | addToWrapper(name, _[name] = obj[name]); 606 | }); 607 | }; 608 | 609 | // Generate a unique integer id (unique within the entire client session). 610 | // Useful for temporary DOM ids. 611 | var idCounter = 0; 612 | _.uniqueId = function(prefix) { 613 | var id = idCounter++; 614 | return prefix ? prefix + id : id; 615 | }; 616 | 617 | // By default, Underscore uses ERB-style template delimiters, change the 618 | // following template settings to use alternative delimiters. 619 | _.templateSettings = { 620 | start : '<%', 621 | end : '%>', 622 | interpolate : /<%=(.+?)%>/g 623 | }; 624 | 625 | // JavaScript templating a-la ERB, pilfered from John Resig's 626 | // "Secrets of the JavaScript Ninja", page 83. 627 | // Single-quote fix from Rick Strahl's version. 628 | // With alterations for arbitrary delimiters, and to preserve whitespace. 629 | _.template = function(str, data) { 630 | var c = _.templateSettings; 631 | var endMatch = new RegExp("'(?=[^"+c.end.substr(0, 1)+"]*"+escapeRegExp(c.end)+")","g"); 632 | var fn = new Function('obj', 633 | 'var p=[],print=function(){p.push.apply(p,arguments);};' + 634 | 'with(obj||{}){p.push(\'' + 635 | str.replace(/\r/g, '\\r') 636 | .replace(/\n/g, '\\n') 637 | .replace(/\t/g, '\\t') 638 | .replace(endMatch,"✄") 639 | .split("'").join("\\'") 640 | .split("✄").join("'") 641 | .replace(c.interpolate, "',$1,'") 642 | .split(c.start).join("');") 643 | .split(c.end).join("p.push('") 644 | + "');}return p.join('');"); 645 | return data ? fn(data) : fn; 646 | }; 647 | 648 | // ------------------------------- Aliases ---------------------------------- 649 | 650 | _.each = _.forEach; 651 | _.foldl = _.inject = _.reduce; 652 | _.foldr = _.reduceRight; 653 | _.select = _.filter; 654 | _.all = _.every; 655 | _.any = _.some; 656 | _.contains = _.include; 657 | _.head = _.first; 658 | _.tail = _.rest; 659 | _.methods = _.functions; 660 | 661 | // ------------------------ Setup the OOP Wrapper: -------------------------- 662 | 663 | // If Underscore is called as a function, it returns a wrapped object that 664 | // can be used OO-style. This wrapper holds altered versions of all the 665 | // underscore functions. Wrapped objects may be chained. 666 | var wrapper = function(obj) { this._wrapped = obj; }; 667 | 668 | // Helper function to continue chaining intermediate results. 669 | var result = function(obj, chain) { 670 | return chain ? _(obj).chain() : obj; 671 | }; 672 | 673 | // A method to easily add functions to the OOP wrapper. 674 | var addToWrapper = function(name, func) { 675 | wrapper.prototype[name] = function() { 676 | var args = _.toArray(arguments); 677 | unshift.call(args, this._wrapped); 678 | return result(func.apply(_, args), this._chain); 679 | }; 680 | }; 681 | 682 | // Add all of the Underscore functions to the wrapper object. 683 | _.mixin(_); 684 | 685 | // Add all mutator Array functions to the wrapper. 686 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 687 | var method = ArrayProto[name]; 688 | wrapper.prototype[name] = function() { 689 | method.apply(this._wrapped, arguments); 690 | return result(this._wrapped, this._chain); 691 | }; 692 | }); 693 | 694 | // Add all accessor Array functions to the wrapper. 695 | each(['concat', 'join', 'slice'], function(name) { 696 | var method = ArrayProto[name]; 697 | wrapper.prototype[name] = function() { 698 | return result(method.apply(this._wrapped, arguments), this._chain); 699 | }; 700 | }); 701 | 702 | // Start chaining a wrapped Underscore object. 703 | wrapper.prototype.chain = function() { 704 | this._chain = true; 705 | return this; 706 | }; 707 | 708 | // Extracts the result from a wrapped and chained object. 709 | wrapper.prototype.value = function() { 710 | return this._wrapped; 711 | }; 712 | 713 | })(); 714 | -------------------------------------------------------------------------------- /public/js/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.3.1 2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Backbone may be freely distributed under the MIT license. 4 | // For all details and documentation: 5 | // http://documentcloud.github.com/backbone 6 | 7 | (function(){ 8 | 9 | // Initial Setup 10 | // ------------- 11 | 12 | // The top-level namespace. All public Backbone classes and modules will 13 | // be attached to this. Exported for both CommonJS and the browser. 14 | var Backbone; 15 | if (typeof exports !== 'undefined') { 16 | Backbone = exports; 17 | } else { 18 | Backbone = this.Backbone = {}; 19 | } 20 | 21 | // Current version of the library. Keep in sync with `package.json`. 22 | Backbone.VERSION = '0.3.1'; 23 | 24 | // Require Underscore, if we're on the server, and it's not already present. 25 | var _ = this._; 26 | if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; 27 | 28 | // For Backbone's purposes, jQuery owns the `$` variable. 29 | var $ = this.jQuery; 30 | 31 | // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will 32 | // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a 33 | // `X-Http-Method-Override` header. 34 | Backbone.emulateHTTP = false; 35 | 36 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 37 | // `application/json` requests ... will encode the body as 38 | // `application/x-www-form-urlencoded` instead and will send the model in a 39 | // form param named `model`. 40 | Backbone.emulateJSON = false; 41 | 42 | // Backbone.Events 43 | // ----------------- 44 | 45 | // A module that can be mixed in to *any object* in order to provide it with 46 | // custom events. You may `bind` or `unbind` a callback function to an event; 47 | // `trigger`-ing an event fires all callbacks in succession. 48 | // 49 | // var object = {}; 50 | // _.extend(object, Backbone.Events); 51 | // object.bind('expand', function(){ alert('expanded'); }); 52 | // object.trigger('expand'); 53 | // 54 | Backbone.Events = { 55 | 56 | // Bind an event, specified by a string name, `ev`, to a `callback` function. 57 | // Passing `"all"` will bind the callback to all events fired. 58 | bind : function(ev, callback) { 59 | var calls = this._callbacks || (this._callbacks = {}); 60 | var list = this._callbacks[ev] || (this._callbacks[ev] = []); 61 | list.push(callback); 62 | return this; 63 | }, 64 | 65 | // Remove one or many callbacks. If `callback` is null, removes all 66 | // callbacks for the event. If `ev` is null, removes all bound callbacks 67 | // for all events. 68 | unbind : function(ev, callback) { 69 | var calls; 70 | if (!ev) { 71 | this._callbacks = {}; 72 | } else if (calls = this._callbacks) { 73 | if (!callback) { 74 | calls[ev] = []; 75 | } else { 76 | var list = calls[ev]; 77 | if (!list) return this; 78 | for (var i = 0, l = list.length; i < l; i++) { 79 | if (callback === list[i]) { 80 | list.splice(i, 1); 81 | break; 82 | } 83 | } 84 | } 85 | } 86 | return this; 87 | }, 88 | 89 | // Trigger an event, firing all bound callbacks. Callbacks are passed the 90 | // same arguments as `trigger` is, apart from the event name. 91 | // Listening for `"all"` passes the true event name as the first argument. 92 | trigger : function(ev) { 93 | var list, calls, i, l; 94 | if (!(calls = this._callbacks)) return this; 95 | if (list = calls[ev]) { 96 | for (i = 0, l = list.length; i < l; i++) { 97 | list[i].apply(this, Array.prototype.slice.call(arguments, 1)); 98 | } 99 | } 100 | if (list = calls['all']) { 101 | for (i = 0, l = list.length; i < l; i++) { 102 | list[i].apply(this, arguments); 103 | } 104 | } 105 | return this; 106 | } 107 | 108 | }; 109 | 110 | // Backbone.Model 111 | // -------------- 112 | 113 | // Create a new model, with defined attributes. A client id (`cid`) 114 | // is automatically generated and assigned for you. 115 | Backbone.Model = function(attributes, options) { 116 | this.attributes = {}; 117 | this.cid = _.uniqueId('c'); 118 | this.set(attributes || {}, {silent : true}); 119 | this._previousAttributes = _.clone(this.attributes); 120 | if (options && options.collection) this.collection = options.collection; 121 | this.initialize(attributes, options); 122 | }; 123 | 124 | // Attach all inheritable methods to the Model prototype. 125 | _.extend(Backbone.Model.prototype, Backbone.Events, { 126 | 127 | // A snapshot of the model's previous attributes, taken immediately 128 | // after the last `"change"` event was fired. 129 | _previousAttributes : null, 130 | 131 | // Has the item been changed since the last `"change"` event? 132 | _changed : false, 133 | 134 | // Initialize is an empty function by default. Override it with your own 135 | // initialization logic. 136 | initialize : function(){}, 137 | 138 | // Return a copy of the model's `attributes` object. 139 | toJSON : function() { 140 | return _.clone(this.attributes); 141 | }, 142 | 143 | // Get the value of an attribute. 144 | get : function(attr) { 145 | return this.attributes[attr]; 146 | }, 147 | 148 | // Set a hash of model attributes on the object, firing `"change"` unless you 149 | // choose to silence it. 150 | set : function(attrs, options) { 151 | 152 | // Extract attributes and options. 153 | options || (options = {}); 154 | if (!attrs) return this; 155 | if (attrs.attributes) attrs = attrs.attributes; 156 | var now = this.attributes; 157 | 158 | // Run validation. 159 | if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; 160 | 161 | // Check for changes of `id`. 162 | if ('id' in attrs) this.id = attrs.id; 163 | 164 | // Update attributes. 165 | for (var attr in attrs) { 166 | var val = attrs[attr]; 167 | if (!_.isEqual(now[attr], val)) { 168 | now[attr] = val; 169 | if (!options.silent) { 170 | this._changed = true; 171 | this.trigger('change:' + attr, this, val); 172 | } 173 | } 174 | } 175 | 176 | // Fire the `"change"` event, if the model has been changed. 177 | if (!options.silent && this._changed) this.change(); 178 | return this; 179 | }, 180 | 181 | // Remove an attribute from the model, firing `"change"` unless you choose 182 | // to silence it. 183 | unset : function(attr, options) { 184 | options || (options = {}); 185 | var value = this.attributes[attr]; 186 | 187 | // Run validation. 188 | var validObj = {}; 189 | validObj[attr] = void 0; 190 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 191 | 192 | // Remove the attribute. 193 | delete this.attributes[attr]; 194 | if (!options.silent) { 195 | this._changed = true; 196 | this.trigger('change:' + attr, this); 197 | this.change(); 198 | } 199 | return this; 200 | }, 201 | 202 | // Clear all attributes on the model, firing `"change"` unless you choose 203 | // to silence it. 204 | clear : function(options) { 205 | options || (options = {}); 206 | var old = this.attributes; 207 | 208 | // Run validation. 209 | var validObj = {}; 210 | for (attr in old) validObj[attr] = void 0; 211 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 212 | 213 | this.attributes = {}; 214 | if (!options.silent) { 215 | this._changed = true; 216 | for (attr in old) { 217 | this.trigger('change:' + attr, this); 218 | } 219 | this.change(); 220 | } 221 | return this; 222 | }, 223 | 224 | // Fetch the model from the server. If the server's representation of the 225 | // model differs from its current attributes, they will be overriden, 226 | // triggering a `"change"` event. 227 | fetch : function(options) { 228 | options || (options = {}); 229 | var model = this; 230 | var success = function(resp) { 231 | if (!model.set(model.parse(resp), options)) return false; 232 | if (options.success) options.success(model, resp); 233 | }; 234 | var error = options.error && _.bind(options.error, null, model); 235 | Backbone.sync('read', this, success, error); 236 | return this; 237 | }, 238 | 239 | // Set a hash of model attributes, and sync the model to the server. 240 | // If the server returns an attributes hash that differs, the model's 241 | // state will be `set` again. 242 | save : function(attrs, options) { 243 | attrs || (attrs = {}); 244 | options || (options = {}); 245 | if (!this.set(attrs, options)) return false; 246 | var model = this; 247 | var success = function(resp) { 248 | if (!model.set(model.parse(resp), options)) return false; 249 | if (options.success) options.success(model, resp); 250 | }; 251 | var error = options.error && _.bind(options.error, null, model); 252 | var method = this.isNew() ? 'create' : 'update'; 253 | Backbone.sync(method, this, success, error); 254 | return this; 255 | }, 256 | 257 | // Destroy this model on the server. Upon success, the model is removed 258 | // from its collection, if it has one. 259 | destroy : function(options) { 260 | options || (options = {}); 261 | var model = this; 262 | var success = function(resp) { 263 | if (model.collection) model.collection.remove(model); 264 | if (options.success) options.success(model, resp); 265 | }; 266 | var error = options.error && _.bind(options.error, null, model); 267 | Backbone.sync('delete', this, success, error); 268 | return this; 269 | }, 270 | 271 | // Default URL for the model's representation on the server -- if you're 272 | // using Backbone's restful methods, override this to change the endpoint 273 | // that will be called. 274 | url : function() { 275 | var base = getUrl(this.collection); 276 | if (this.isNew()) return base; 277 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id; 278 | }, 279 | 280 | // **parse** converts a response into the hash of attributes to be `set` on 281 | // the model. The default implementation is just to pass the response along. 282 | parse : function(resp) { 283 | return resp; 284 | }, 285 | 286 | // Create a new model with identical attributes to this one. 287 | clone : function() { 288 | return new this.constructor(this); 289 | }, 290 | 291 | // A model is new if it has never been saved to the server, and has a negative 292 | // ID. 293 | isNew : function() { 294 | return !this.id; 295 | }, 296 | 297 | // Call this method to manually fire a `change` event for this model. 298 | // Calling this will cause all objects observing the model to update. 299 | change : function() { 300 | this.trigger('change', this); 301 | this._previousAttributes = _.clone(this.attributes); 302 | this._changed = false; 303 | }, 304 | 305 | // Determine if the model has changed since the last `"change"` event. 306 | // If you specify an attribute name, determine if that attribute has changed. 307 | hasChanged : function(attr) { 308 | if (attr) return this._previousAttributes[attr] != this.attributes[attr]; 309 | return this._changed; 310 | }, 311 | 312 | // Return an object containing all the attributes that have changed, or false 313 | // if there are no changed attributes. Useful for determining what parts of a 314 | // view need to be updated and/or what attributes need to be persisted to 315 | // the server. 316 | changedAttributes : function(now) { 317 | now || (now = this.attributes); 318 | var old = this._previousAttributes; 319 | var changed = false; 320 | for (var attr in now) { 321 | if (!_.isEqual(old[attr], now[attr])) { 322 | changed = changed || {}; 323 | changed[attr] = now[attr]; 324 | } 325 | } 326 | return changed; 327 | }, 328 | 329 | // Get the previous value of an attribute, recorded at the time the last 330 | // `"change"` event was fired. 331 | previous : function(attr) { 332 | if (!attr || !this._previousAttributes) return null; 333 | return this._previousAttributes[attr]; 334 | }, 335 | 336 | // Get all of the attributes of the model at the time of the previous 337 | // `"change"` event. 338 | previousAttributes : function() { 339 | return _.clone(this._previousAttributes); 340 | }, 341 | 342 | // Run validation against a set of incoming attributes, returning `true` 343 | // if all is well. If a specific `error` callback has been passed, 344 | // call that instead of firing the general `"error"` event. 345 | _performValidation : function(attrs, options) { 346 | var error = this.validate(attrs); 347 | if (error) { 348 | if (options.error) { 349 | options.error(this, error); 350 | } else { 351 | this.trigger('error', this, error); 352 | } 353 | return false; 354 | } 355 | return true; 356 | } 357 | 358 | }); 359 | 360 | // Backbone.Collection 361 | // ------------------- 362 | 363 | // Provides a standard collection class for our sets of models, ordered 364 | // or unordered. If a `comparator` is specified, the Collection will maintain 365 | // its models in sort order, as they're added and removed. 366 | Backbone.Collection = function(models, options) { 367 | options || (options = {}); 368 | if (options.comparator) { 369 | this.comparator = options.comparator; 370 | delete options.comparator; 371 | } 372 | this._boundOnModelEvent = _.bind(this._onModelEvent, this); 373 | this._reset(); 374 | if (models) this.refresh(models, {silent: true}); 375 | this.initialize(models, options); 376 | }; 377 | 378 | // Define the Collection's inheritable methods. 379 | _.extend(Backbone.Collection.prototype, Backbone.Events, { 380 | 381 | // The default model for a collection is just a **Backbone.Model**. 382 | // This should be overridden in most cases. 383 | model : Backbone.Model, 384 | 385 | // Initialize is an empty function by default. Override it with your own 386 | // initialization logic. 387 | initialize : function(){}, 388 | 389 | // The JSON representation of a Collection is an array of the 390 | // models' attributes. 391 | toJSON : function() { 392 | return this.map(function(model){ return model.toJSON(); }); 393 | }, 394 | 395 | // Add a model, or list of models to the set. Pass **silent** to avoid 396 | // firing the `added` event for every new model. 397 | add : function(models, options) { 398 | if (_.isArray(models)) { 399 | for (var i = 0, l = models.length; i < l; i++) { 400 | this._add(models[i], options); 401 | } 402 | } else { 403 | this._add(models, options); 404 | } 405 | return this; 406 | }, 407 | 408 | // Remove a model, or a list of models from the set. Pass silent to avoid 409 | // firing the `removed` event for every model removed. 410 | remove : function(models, options) { 411 | if (_.isArray(models)) { 412 | for (var i = 0, l = models.length; i < l; i++) { 413 | this._remove(models[i], options); 414 | } 415 | } else { 416 | this._remove(models, options); 417 | } 418 | return this; 419 | }, 420 | 421 | // Get a model from the set by id. 422 | get : function(id) { 423 | if (id == null) return null; 424 | return this._byId[id.id != null ? id.id : id]; 425 | }, 426 | 427 | // Get a model from the set by client id. 428 | getByCid : function(cid) { 429 | return cid && this._byCid[cid.cid || cid]; 430 | }, 431 | 432 | // Get the model at the given index. 433 | at: function(index) { 434 | return this.models[index]; 435 | }, 436 | 437 | // Force the collection to re-sort itself. You don't need to call this under normal 438 | // circumstances, as the set will maintain sort order as each item is added. 439 | sort : function(options) { 440 | options || (options = {}); 441 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 442 | this.models = this.sortBy(this.comparator); 443 | if (!options.silent) this.trigger('refresh', this); 444 | return this; 445 | }, 446 | 447 | // Pluck an attribute from each model in the collection. 448 | pluck : function(attr) { 449 | return _.map(this.models, function(model){ return model.get(attr); }); 450 | }, 451 | 452 | // When you have more items than you want to add or remove individually, 453 | // you can refresh the entire set with a new list of models, without firing 454 | // any `added` or `removed` events. Fires `refresh` when finished. 455 | refresh : function(models, options) { 456 | models || (models = []); 457 | options || (options = {}); 458 | this._reset(); 459 | this.add(models, {silent: true}); 460 | if (!options.silent) this.trigger('refresh', this); 461 | return this; 462 | }, 463 | 464 | // Fetch the default set of models for this collection, refreshing the 465 | // collection when they arrive. 466 | fetch : function(options) { 467 | options || (options = {}); 468 | var collection = this; 469 | var success = function(resp) { 470 | collection.refresh(collection.parse(resp)); 471 | if (options.success) options.success(collection, resp); 472 | }; 473 | var error = options.error && _.bind(options.error, null, collection); 474 | Backbone.sync('read', this, success, error); 475 | return this; 476 | }, 477 | 478 | // Create a new instance of a model in this collection. After the model 479 | // has been created on the server, it will be added to the collection. 480 | create : function(model, options) { 481 | var coll = this; 482 | options || (options = {}); 483 | if (!(model instanceof Backbone.Model)) { 484 | model = new this.model(model, {collection: coll}); 485 | } else { 486 | model.collection = coll; 487 | } 488 | var success = function(nextModel, resp) { 489 | coll.add(nextModel); 490 | if (options.success) options.success(nextModel, resp); 491 | }; 492 | return model.save(null, {success : success, error : options.error}); 493 | }, 494 | 495 | // **parse** converts a response into a list of models to be added to the 496 | // collection. The default implementation is just to pass it through. 497 | parse : function(resp) { 498 | return resp; 499 | }, 500 | 501 | // Proxy to _'s chain. Can't be proxied the same way the rest of the 502 | // underscore methods are proxied because it relies on the underscore 503 | // constructor. 504 | chain: function () { 505 | return _(this.models).chain(); 506 | }, 507 | 508 | // Reset all internal state. Called when the collection is refreshed. 509 | _reset : function(options) { 510 | this.length = 0; 511 | this.models = []; 512 | this._byId = {}; 513 | this._byCid = {}; 514 | }, 515 | 516 | // Internal implementation of adding a single model to the set, updating 517 | // hash indexes for `id` and `cid` lookups. 518 | _add : function(model, options) { 519 | options || (options = {}); 520 | if (!(model instanceof Backbone.Model)) { 521 | model = new this.model(model, {collection: this}); 522 | } 523 | var already = this.getByCid(model); 524 | if (already) throw new Error(["Can't add the same model to a set twice", already.id]); 525 | this._byId[model.id] = model; 526 | this._byCid[model.cid] = model; 527 | model.collection = this; 528 | var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; 529 | this.models.splice(index, 0, model); 530 | model.bind('all', this._boundOnModelEvent); 531 | this.length++; 532 | if (!options.silent) model.trigger('add', model, this); 533 | return model; 534 | }, 535 | 536 | // Internal implementation of removing a single model from the set, updating 537 | // hash indexes for `id` and `cid` lookups. 538 | _remove : function(model, options) { 539 | options || (options = {}); 540 | model = this.getByCid(model) || this.get(model); 541 | if (!model) return null; 542 | delete this._byId[model.id]; 543 | delete this._byCid[model.cid]; 544 | delete model.collection; 545 | this.models.splice(this.indexOf(model), 1); 546 | this.length--; 547 | if (!options.silent) model.trigger('remove', model, this); 548 | model.unbind('all', this._boundOnModelEvent); 549 | return model; 550 | }, 551 | 552 | // Internal method called every time a model in the set fires an event. 553 | // Sets need to update their indexes when models change ids. All other 554 | // events simply proxy through. 555 | _onModelEvent : function(ev, model) { 556 | if (ev === 'change:id') { 557 | delete this._byId[model.previous('id')]; 558 | this._byId[model.id] = model; 559 | } 560 | this.trigger.apply(this, arguments); 561 | } 562 | 563 | }); 564 | 565 | // Underscore methods that we want to implement on the Collection. 566 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 567 | 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 568 | 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 569 | 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; 570 | 571 | // Mix in each Underscore method as a proxy to `Collection#models`. 572 | _.each(methods, function(method) { 573 | Backbone.Collection.prototype[method] = function() { 574 | return _[method].apply(_, [this.models].concat(_.toArray(arguments))); 575 | }; 576 | }); 577 | 578 | // Backbone.Controller 579 | // ------------------- 580 | 581 | // Controllers map faux-URLs to actions, and fire events when routes are 582 | // matched. Creating a new one sets its `routes` hash, if not set statically. 583 | Backbone.Controller = function(options) { 584 | options || (options = {}); 585 | if (options.routes) this.routes = options.routes; 586 | this._bindRoutes(); 587 | this.initialize(options); 588 | }; 589 | 590 | // Cached regular expressions for matching named param parts and splatted 591 | // parts of route strings. 592 | var namedParam = /:([\w\d]+)/g; 593 | var splatParam = /\*([\w\d]+)/g; 594 | 595 | // Set up all inheritable **Backbone.Controller** properties and methods. 596 | _.extend(Backbone.Controller.prototype, Backbone.Events, { 597 | 598 | // Initialize is an empty function by default. Override it with your own 599 | // initialization logic. 600 | initialize : function(){}, 601 | 602 | // Manually bind a single named route to a callback. For example: 603 | // 604 | // this.route('search/:query/p:num', 'search', function(query, num) { 605 | // ... 606 | // }); 607 | // 608 | route : function(route, name, callback) { 609 | Backbone.history || (Backbone.history = new Backbone.History); 610 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 611 | Backbone.history.route(route, _.bind(function(fragment) { 612 | var args = this._extractParameters(route, fragment); 613 | callback.apply(this, args); 614 | this.trigger.apply(this, ['route:' + name].concat(args)); 615 | }, this)); 616 | }, 617 | 618 | // Simple proxy to `Backbone.history` to save a fragment into the history, 619 | // without triggering routes. 620 | saveLocation : function(fragment) { 621 | Backbone.history.saveLocation(fragment); 622 | }, 623 | 624 | // Bind all defined routes to `Backbone.history`. 625 | _bindRoutes : function() { 626 | if (!this.routes) return; 627 | for (var route in this.routes) { 628 | var name = this.routes[route]; 629 | this.route(route, name, this[name]); 630 | } 631 | }, 632 | 633 | // Convert a route string into a regular expression, suitable for matching 634 | // against the current location fragment. 635 | _routeToRegExp : function(route) { 636 | route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)"); 637 | return new RegExp('^' + route + '$'); 638 | }, 639 | 640 | // Given a route, and a URL fragment that it matches, return the array of 641 | // extracted parameters. 642 | _extractParameters : function(route, fragment) { 643 | return route.exec(fragment).slice(1); 644 | } 645 | 646 | }); 647 | 648 | // Backbone.History 649 | // ---------------- 650 | 651 | // Handles cross-browser history management, based on URL hashes. If the 652 | // browser does not support `onhashchange`, falls back to polling. 653 | Backbone.History = function() { 654 | this.handlers = []; 655 | this.fragment = this.getFragment(); 656 | _.bindAll(this, 'checkUrl'); 657 | }; 658 | 659 | // Cached regex for cleaning hashes. 660 | var hashStrip = /^#*/; 661 | 662 | // Set up all inheritable **Backbone.History** properties and methods. 663 | _.extend(Backbone.History.prototype, { 664 | 665 | // The default interval to poll for hash changes, if necessary, is 666 | // twenty times a second. 667 | interval: 50, 668 | 669 | // Get the cross-browser normalized URL fragment. 670 | getFragment : function(loc) { 671 | return (loc || window.location).hash.replace(hashStrip, ''); 672 | }, 673 | 674 | // Start the hash change handling, returning `true` if the current URL matches 675 | // an existing route, and `false` otherwise. 676 | start : function() { 677 | var docMode = document.documentMode; 678 | var oldIE = ($.browser.msie && docMode < 7); 679 | if (oldIE) { 680 | this.iframe = $('