├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE.md ├── bower.json ├── component.json ├── lib ├── backbone.babysitter.js ├── backbone.babysitter.min.js └── backbone.babysitter.min.js.map ├── package.json ├── public └── javascripts │ ├── backbone.js │ ├── jquery.js │ ├── json2.js │ └── underscore.js ├── readme.md ├── spec └── javascripts │ ├── childviewContainer.spec.js │ └── helpers │ └── jasmineMatchers.js └── src ├── build └── backbone.babysitter.js └── childviewcontainer.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *.swo 4 | .grunt/ 5 | node_modules 6 | _SpecRunner.html 7 | .idea 8 | npm-debug.log 9 | bower_components 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": false, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "browser": true, 13 | "globals": { 14 | "jQuery": true, 15 | "Backbone": true, 16 | "_": true, 17 | "Marionette": true, 18 | "$": true, 19 | "slice": true, 20 | "require": true, 21 | "define": true, 22 | "module": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: npm install -g grunt-cli 3 | node_js: 4 | - "0.10" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ### v1.0.0 4 | 5 | * Updated Backbone and Underscore version ranges again 6 | 7 | ### v1.0.0-pre.2 8 | 9 | * Updated Backbone and Underscore version ranges 10 | 11 | ### v1.0.0-pre.1 12 | 13 | * Moved Backbone and Underscore to peerDependencies 14 | 15 | ### v0.1.12 16 | 17 | * Bump version range of backbone support. 18 | 19 | ### v0.1.11 20 | 21 | * Bump version range of backbone support. 22 | 23 | ### v0.1.10 24 | 25 | * Bump version range of backbone support. 26 | 27 | ### v0.1.9 28 | 29 | * Bump version range of backbone support. 30 | 31 | ### v0.1.8 32 | 33 | * Bump version range of backbone and underscore support. 34 | 35 | ### v0.1.7 36 | 37 | * Bump version range of backbone support. 38 | 39 | ### v0.1.6 40 | 41 | * Expose `reduce` to babysitter collections. Thanks @romanbsd 42 | 43 | ### v0.1.5 44 | 45 | * Minor updates to bower.json 46 | 47 | ### v0.1.4 48 | 49 | * Update UMD Wrapper and build process 50 | 51 | ### v0.1.2 52 | 53 | * Add .VERSION and n.oConflict 54 | * General cleanups to tests and package.json 55 | * Add travis build info 56 | 57 | ### v0.1.1 58 | * Remove AMD builds and replace with a single UMD style wrapper. 59 | 60 | ### v0.1.0 61 | * allow chaining of add and remove methods 62 | * add component.json 63 | 64 | #### General 65 | * update grunt file 66 | * readme fixed 67 | * fix gruntfile url 68 | 69 | ### v0.0.6 70 | 71 | * Removed `.findByCollection` method 72 | * Added `.findByModelCid` method 73 | 74 | ### v0.0.5 75 | 76 | * Updated build process to use GruntJS v0.4 77 | 78 | ### v0.0.4 79 | 80 | * Added a fix for IE < 9, when applying a function to the views 81 | * Added `.pluck` as a method, from Underscore.js 82 | * Can specify an array of views to the container constructor 83 | 84 | ### v0.0.3 85 | 86 | * Added iterators and other collection processing functions from Underscore.js 87 | 88 | ### v0.0.2 89 | 90 | * Added `.length` attribute 91 | * Added `.findByIndex` method 92 | 93 | ### v0.0.1 94 | 95 | * Initial release 96 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | meta: { 8 | version: '<%= pkg.version %>', 9 | banner: 10 | '// Backbone.BabySitter\n' + 11 | '// -------------------\n' + 12 | '// v<%= pkg.version %>\n' + 13 | '//\n' + 14 | '// Copyright (c)<%= grunt.template.today("yyyy") %> Derick Bailey, Muted Solutions, LLC.\n' + 15 | '// Distributed under MIT license\n' + 16 | '//\n' + 17 | '// http://github.com/marionettejs/backbone.babysitter\n' + 18 | '\n' 19 | }, 20 | 21 | lint: { 22 | files: ['src/*.js'] 23 | }, 24 | 25 | preprocess: { 26 | umd: { 27 | src: 'src/build/backbone.babysitter.js', 28 | dest: 'lib/backbone.babysitter.js' 29 | } 30 | }, 31 | 32 | template: { 33 | options: { 34 | data: { 35 | version: '<%= meta.version %>' 36 | } 37 | }, 38 | umd: { 39 | src: '<%= preprocess.umd.dest %>', 40 | dest: '<%= preprocess.umd.dest %>' 41 | } 42 | }, 43 | 44 | concat: { 45 | options: { 46 | banner: "<%= meta.banner %>" 47 | }, 48 | umd: { 49 | src: '<%= preprocess.umd.dest %>', 50 | dest: '<%= preprocess.umd.dest %>' 51 | } 52 | }, 53 | 54 | uglify : { 55 | options: { 56 | banner: "<%= meta.banner %>" 57 | }, 58 | umd : { 59 | src : 'lib/backbone.babysitter.js', 60 | dest : 'lib/backbone.babysitter.min.js', 61 | options : { 62 | sourceMap : 'lib/backbone.babysitter.map', 63 | sourceMappingURL : 'backbone.babysitter.map', 64 | sourceMapPrefix : 2 65 | } 66 | } 67 | }, 68 | 69 | jasmine : { 70 | options : { 71 | helpers : 'spec/javascripts/helpers/*.js', 72 | specs : 'spec/javascripts/**/*.spec.js', 73 | vendor : [ 74 | 'public/javascripts/jquery.js', 75 | 'public/javascripts/json2.js', 76 | 'public/javascripts/underscore.js', 77 | 'public/javascripts/backbone.js' 78 | ], 79 | }, 80 | babysitter : { 81 | src : ['src/*.js'] 82 | } 83 | }, 84 | 85 | jshint: { 86 | options: { 87 | jshintrc : '.jshintrc' 88 | }, 89 | babysitter : [ 'src/*.js' ] 90 | }, 91 | 92 | watch: { 93 | babysitter : { 94 | files : ['src/*.js', 'spec/**/*.js'], 95 | tasks : ['jshint', 'jasmine:babysitter'] 96 | }, 97 | server : { 98 | files : ['src/*.js', 'spec/**/*.js'], 99 | tasks : ['jasmine:babysitter:build'] 100 | } 101 | }, 102 | 103 | connect: { 104 | server: { 105 | options: { 106 | port: 8888 107 | } 108 | } 109 | } 110 | }); 111 | 112 | grunt.loadNpmTasks('grunt-preprocess'); 113 | grunt.loadNpmTasks('grunt-template'); 114 | grunt.loadNpmTasks('grunt-contrib-jasmine'); 115 | grunt.loadNpmTasks('grunt-contrib-concat'); 116 | grunt.loadNpmTasks('grunt-contrib-jshint'); 117 | grunt.loadNpmTasks('grunt-contrib-uglify'); 118 | grunt.loadNpmTasks('grunt-contrib-watch'); 119 | grunt.loadNpmTasks('grunt-contrib-connect'); 120 | 121 | grunt.registerTask('test', ['jshint', 'jasmine:babysitter']); 122 | 123 | grunt.registerTask('dev', ['test', 'watch:babysitter']); 124 | 125 | grunt.registerTask('server', ['jasmine:babysitter:build', 'connect:server', 'watch:server']); 126 | 127 | // Default task. 128 | grunt.registerTask('default', ['jshint', 'jasmine:babysitter', 'preprocess', 'template', 'concat', 'uglify']); 129 | 130 | }; 131 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Backbone.BabySitter 2 | 3 | Copyright (C)2013 Derick Bailey, Muted Solutions, LLC 4 | 5 | Distributed Under [MIT License](http://mutedsolutions.mit-license.org/) 6 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.babysitter", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/marionettejs/backbone.babysitter", 5 | "authors": [ 6 | "Derick Bailey " 7 | ], 8 | "description": "Manage child views in a Backbone.View", 9 | "main": "lib/backbone.babysitter.js", 10 | "keywords": [ 11 | "backbone", 12 | "plugin", 13 | "computed", 14 | "field", 15 | "model", 16 | "client", 17 | "browser" 18 | ], 19 | "dependencies": { 20 | "backbone": ">=1.3.3", 21 | "underscore": ">=1.8.3" 22 | }, 23 | "license": "MIT", 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests", 30 | "public", 31 | "spec", 32 | ".gitignore", 33 | ".jshintrc", 34 | ".travis.yml", 35 | "Gruntfile.js", 36 | "component.json", 37 | "package.json" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.babysitter", 3 | "description": "Manage child views in a Backbone.View", 4 | "version": "0.1.12", 5 | "repo": "marionettejs/backbone.babysitter", 6 | "main": "lib/backbone.babysitter.js", 7 | "keywords": [ 8 | "backbone", 9 | "plugin", 10 | "computed", 11 | "field", 12 | "model", 13 | "client", 14 | "browser" 15 | ], 16 | "license": "MIT", 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/marionettejs/backbone.babysitter/blob/master/LICENSE.md" 21 | } 22 | ], 23 | "scripts": [ 24 | "lib/backbone.babysitter.js" 25 | ], 26 | "author": { 27 | "name": "Derick Bailey", 28 | "email": "marionettejs@gmail.com", 29 | "web": "http://derickbailey.lostechies.com" 30 | }, 31 | "dependencies": { 32 | "jashkenas/backbone": ">=0.9.9 <=1.3.x", 33 | "jashkenas/underscore": ">=1.4.0 <=1.8.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/backbone.babysitter.js: -------------------------------------------------------------------------------- 1 | // Backbone.BabySitter 2 | // ------------------- 3 | // v1.0.0 4 | // 5 | // Copyright (c)2019 Derick Bailey, Muted Solutions, LLC. 6 | // Distributed under MIT license 7 | // 8 | // http://github.com/marionettejs/backbone.babysitter 9 | 10 | (function(root, factory) { 11 | 12 | if (typeof define === 'function' && define.amd) { 13 | define(['backbone', 'underscore'], function(Backbone, _) { 14 | return factory(Backbone, _); 15 | }); 16 | } else if (typeof exports !== 'undefined') { 17 | var Backbone = require('backbone'); 18 | var _ = require('underscore'); 19 | module.exports = factory(Backbone, _); 20 | } else { 21 | factory(root.Backbone, root._); 22 | } 23 | 24 | }(this, function(Backbone, _) { 25 | 'use strict'; 26 | 27 | var previousChildViewContainer = Backbone.ChildViewContainer; 28 | 29 | // BabySitter.ChildViewContainer 30 | // ----------------------------- 31 | // 32 | // Provide a container to store, retrieve and 33 | // shut down child views. 34 | 35 | Backbone.ChildViewContainer = (function (Backbone, _) { 36 | 37 | // Container Constructor 38 | // --------------------- 39 | 40 | var Container = function(views){ 41 | this._views = {}; 42 | this._indexByModel = {}; 43 | this._indexByCustom = {}; 44 | this._updateLength(); 45 | 46 | _.each(views, _.bind(this.add, this)); 47 | }; 48 | 49 | // Container Methods 50 | // ----------------- 51 | 52 | _.extend(Container.prototype, { 53 | 54 | // Add a view to this container. Stores the view 55 | // by `cid` and makes it searchable by the model 56 | // cid (and model itself). Optionally specify 57 | // a custom key to store an retrieve the view. 58 | add: function(view, customIndex){ 59 | return this._add(view, customIndex) 60 | ._updateLength(); 61 | }, 62 | 63 | // Find a view by the model that was attached to 64 | // it. Uses the model's `cid` to find it. 65 | findByModel: function(model){ 66 | return this.findByModelCid(model.cid); 67 | }, 68 | 69 | // Find a view by the `cid` of the model that was attached to 70 | // it. Uses the model's `cid` to find the view `cid` and 71 | // retrieve the view using it. 72 | findByModelCid: function(modelCid){ 73 | var viewCid = this._indexByModel[modelCid]; 74 | return this.findByCid(viewCid); 75 | }, 76 | 77 | // Find a view by a custom indexer. 78 | findByCustom: function(index){ 79 | var viewCid = this._indexByCustom[index]; 80 | return this.findByCid(viewCid); 81 | }, 82 | 83 | // Find by index. This is not guaranteed to be a 84 | // stable index. 85 | findByIndex: function(index){ 86 | return _.values(this._views)[index]; 87 | }, 88 | 89 | // retrieve a view by its `cid` directly 90 | findByCid: function(cid){ 91 | return this._views[cid]; 92 | }, 93 | 94 | // Remove a view 95 | remove: function(view){ 96 | return this._remove(view) 97 | ._updateLength(); 98 | }, 99 | 100 | // Call a method on every view in the container, 101 | // passing parameters to the call method one at a 102 | // time, like `function.call`. 103 | call: function(method){ 104 | this.apply(method, _.toArray(arguments).slice(1)); 105 | }, 106 | 107 | // Apply a method on every view in the container, 108 | // passing parameters to the call method one at a 109 | // time, like `function.apply`. 110 | apply: function(method, args){ 111 | _.each(this._views, function(view){ 112 | if (_.isFunction(view[method])){ 113 | view[method].apply(view, args || []); 114 | } 115 | }); 116 | }, 117 | 118 | // Update the `.length` attribute on this container 119 | _updateLength: function(){ 120 | this.length = _.size(this._views); 121 | 122 | return this; 123 | }, 124 | // To be used when avoiding call _updateLength 125 | // When you are done adding all your new views 126 | // call _updateLength 127 | _add: function(view, customIndex){ 128 | var viewCid = view.cid; 129 | 130 | // store the view 131 | this._views[viewCid] = view; 132 | 133 | // index it by model 134 | if (view.model){ 135 | this._indexByModel[view.model.cid] = viewCid; 136 | } 137 | 138 | // index by custom 139 | if (customIndex){ 140 | this._indexByCustom[customIndex] = viewCid; 141 | } 142 | 143 | return this; 144 | }, 145 | // To be used when avoiding call _updateLength 146 | // When you are done adding all your new views 147 | // call _updateLength 148 | _remove: function (view){ 149 | var viewCid = view.cid; 150 | 151 | // delete model index 152 | if (view.model){ 153 | delete this._indexByModel[view.model.cid]; 154 | } 155 | 156 | // delete custom index 157 | _.some(this._indexByCustom, _.bind(function(cid, key) { 158 | if (cid === viewCid) { 159 | delete this._indexByCustom[key]; 160 | return true; 161 | } 162 | }, this)); 163 | 164 | // remove the view from the container 165 | delete this._views[viewCid]; 166 | 167 | return this; 168 | } 169 | }); 170 | 171 | // Borrowing this code from Backbone.Collection: 172 | // http://backbonejs.org/docs/backbone.html#section-106 173 | // 174 | // Mix in methods from Underscore, for iteration, and other 175 | // collection related features. 176 | var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', 177 | 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 178 | 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', 179 | 'last', 'without', 'isEmpty', 'pluck', 'reduce']; 180 | 181 | _.each(methods, function(method) { 182 | Container.prototype[method] = function() { 183 | var views = _.values(this._views); 184 | var args = [views].concat(_.toArray(arguments)); 185 | return _[method].apply(_, args); 186 | }; 187 | }); 188 | 189 | // return the public API 190 | return Container; 191 | })(Backbone, _); 192 | 193 | 194 | Backbone.ChildViewContainer.VERSION = '1.0.0'; 195 | 196 | Backbone.ChildViewContainer.noConflict = function () { 197 | Backbone.ChildViewContainer = previousChildViewContainer; 198 | return this; 199 | }; 200 | 201 | return Backbone.ChildViewContainer; 202 | 203 | })); 204 | -------------------------------------------------------------------------------- /lib/backbone.babysitter.min.js: -------------------------------------------------------------------------------- 1 | // Backbone.BabySitter 2 | // ------------------- 3 | // v1.0.0 4 | // 5 | // Copyright (c)2019 Derick Bailey, Muted Solutions, LLC. 6 | // Distributed under MIT license 7 | // 8 | // http://github.com/marionettejs/backbone.babysitter 9 | 10 | 11 | !function(a,b){if("function"==typeof define&&define.amd)define(["backbone","underscore"],function(a,c){return b(a,c)});else if("undefined"!=typeof exports){var c=require("backbone"),d=require("underscore");module.exports=b(c,d)}else b(a.Backbone,a._)}(this,function(a,b){"use strict";var c=a.ChildViewContainer;return a.ChildViewContainer=function(a,b){var c=function(a){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),b.each(a,b.bind(this.add,this))};b.extend(c.prototype,{add:function(a,b){return this._add(a,b)._updateLength()},findByModel:function(a){return this.findByModelCid(a.cid)},findByModelCid:function(a){var b=this._indexByModel[a];return this.findByCid(b)},findByCustom:function(a){var b=this._indexByCustom[a];return this.findByCid(b)},findByIndex:function(a){return b.values(this._views)[a]},findByCid:function(a){return this._views[a]},remove:function(a){return this._remove(a)._updateLength()},call:function(a){this.apply(a,b.toArray(arguments).slice(1))},apply:function(a,c){b.each(this._views,function(d){b.isFunction(d[a])&&d[a].apply(d,c||[])})},_updateLength:function(){return this.length=b.size(this._views),this},_add:function(a,b){var c=a.cid;return this._views[c]=a,a.model&&(this._indexByModel[a.model.cid]=c),b&&(this._indexByCustom[b]=c),this},_remove:function(a){var c=a.cid;return a.model&&delete this._indexByModel[a.model.cid],b.some(this._indexByCustom,b.bind(function(a,b){if(a===c)return delete this._indexByCustom[b],!0},this)),delete this._views[c],this}});var d=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck","reduce"];return b.each(d,function(a){c.prototype[a]=function(){var c=b.values(this._views),d=[c].concat(b.toArray(arguments));return b[a].apply(b,d)}}),c}(a,b),a.ChildViewContainer.VERSION="1.0.0",a.ChildViewContainer.noConflict=function(){return a.ChildViewContainer=c,this},a.ChildViewContainer}); 12 | //# sourceMappingURL=backbone.babysitter.min.js.map -------------------------------------------------------------------------------- /lib/backbone.babysitter.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["backbone.babysitter.js"],"names":["root","factory","define","amd","Backbone","_","exports","require","module","this","previousChildViewContainer","ChildViewContainer","Container","views","_views","_indexByModel","_indexByCustom","_updateLength","each","bind","add","extend","prototype","view","customIndex","_add","findByModel","model","findByModelCid","cid","modelCid","viewCid","findByCid","findByCustom","index","findByIndex","values","remove","_remove","call","method","apply","toArray","arguments","slice","args","isFunction","length","size","some","key","methods","concat","VERSION","noConflict"],"mappings":";;;;;;;;;;CASC,SAASA,EAAMC,GAEd,GAAsB,kBAAXC,SAAyBA,OAAOC,IACzCD,QAAQ,WAAY,cAAe,SAASE,EAAUC,GACpD,MAAOJ,GAAQG,EAAUC,SAEtB,IAAuB,mBAAZC,SAAyB,CACzC,GAAIF,GAAWG,QAAQ,YACnBF,EAAIE,QAAQ,aAChBC,QAAOF,QAAUL,EAAQG,EAAUC,OAEnCJ,GAAQD,EAAKI,SAAUJ,EAAKK,IAG9BI,KAAM,SAASL,EAAUC,GACzB,YAEA,IAAIK,GAA6BN,EAASO,kBA8K1C,OAtKAP,GAASO,mBAAqB,SAAWP,EAAUC,GAKjD,GAAIO,GAAY,SAASC,GACvBJ,KAAKK,UACLL,KAAKM,iBACLN,KAAKO,kBACLP,KAAKQ,gBAELZ,EAAEa,KAAKL,EAAOR,EAAEc,KAAKV,KAAKW,IAAKX,OAMjCJ,GAAEgB,OAAOT,EAAUU,WAMjBF,IAAK,SAASG,EAAMC,GAClB,MAAOf,MAAKgB,KAAKF,EAAMC,GACXP,iBAKdS,YAAa,SAASC,GACpB,MAAOlB,MAAKmB,eAAeD,EAAME,MAMnCD,eAAgB,SAASE,GACvB,GAAIC,GAAUtB,KAAKM,cAAce,EACjC,OAAOrB,MAAKuB,UAAUD,IAIxBE,aAAc,SAASC,GACrB,GAAIH,GAAUtB,KAAKO,eAAekB,EAClC,OAAOzB,MAAKuB,UAAUD,IAKxBI,YAAa,SAASD,GACpB,MAAO7B,GAAE+B,OAAO3B,KAAKK,QAAQoB,IAI/BF,UAAW,SAASH,GAClB,MAAOpB,MAAKK,OAAOe,IAIrBQ,OAAQ,SAASd,GACf,MAAOd,MAAK6B,QAAQf,GACRN,iBAMdsB,KAAM,SAASC,GACb/B,KAAKgC,MAAMD,EAAQnC,EAAEqC,QAAQC,WAAWC,MAAM,KAMhDH,MAAO,SAASD,EAAQK,GACtBxC,EAAEa,KAAKT,KAAKK,OAAQ,SAASS,GACvBlB,EAAEyC,WAAWvB,EAAKiB,KACpBjB,EAAKiB,GAAQC,MAAMlB,EAAMsB,UAM/B5B,cAAe,WAGb,MAFAR,MAAKsC,OAAS1C,EAAE2C,KAAKvC,KAAKK,QAEnBL,MAKTgB,KAAM,SAASF,EAAMC,GACnB,GAAIO,GAAUR,EAAKM,GAenB,OAZApB,MAAKK,OAAOiB,GAAWR,EAGnBA,EAAKI,QACPlB,KAAKM,cAAcQ,EAAKI,MAAME,KAAOE,GAInCP,IACFf,KAAKO,eAAeQ,GAAeO,GAG9BtB,MAKT6B,QAAS,SAAUf,GACjB,GAAIQ,GAAUR,EAAKM,GAkBnB,OAfIN,GAAKI,aACAlB,MAAKM,cAAcQ,EAAKI,MAAME,KAIvCxB,EAAE4C,KAAKxC,KAAKO,eAAgBX,EAAEc,KAAK,SAASU,EAAKqB,GAC/C,GAAIrB,IAAQE,EAEV,aADOtB,MAAKO,eAAekC,IACpB,GAERzC,aAGIA,MAAKK,OAAOiB,GAEZtB,OASX,IAAI0C,IAAW,UAAW,OAAQ,MAAO,OAAQ,SAAU,SACzD,SAAU,SAAU,QAAS,MAAO,OAAQ,MAAO,UACnD,WAAY,SAAU,UAAW,QAAS,UAAW,OACrD,OAAQ,UAAW,UAAW,QAAS,SAWzC,OATA9C,GAAEa,KAAKiC,EAAS,SAASX,GACvB5B,EAAUU,UAAUkB,GAAU,WAC5B,GAAI3B,GAAQR,EAAE+B,OAAO3B,KAAKK,QACtB+B,GAAQhC,GAAOuC,OAAO/C,EAAEqC,QAAQC,WACpC,OAAOtC,GAAEmC,GAAQC,MAAMpC,EAAGwC,MAKvBjC,GACNR,EAAUC,GAGbD,EAASO,mBAAmB0C,QAAU,QAEtCjD,EAASO,mBAAmB2C,WAAa,WAEvC,MADAlD,GAASO,mBAAqBD,EACvBD,MAGFL,EAASO","file":"backbone.babysitter.min.js"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.babysitter", 3 | "description": "Manage child views in a Backbone.View", 4 | "version": "1.0.0", 5 | "homepage": "https://github.com/marionettejs/backbone.babysitter", 6 | "main": "lib/backbone.babysitter.js", 7 | "keywords": [ 8 | "backbone", 9 | "plugin", 10 | "computed", 11 | "field", 12 | "model", 13 | "client", 14 | "browser" 15 | ], 16 | "licenses": [ 17 | { 18 | "type": "MIT", 19 | "url": "https://github.com/marionettejs/backbone.babysitter/blob/master/LICENSE.md" 20 | } 21 | ], 22 | "scripts": { 23 | "test": "grunt jasmine", 24 | "start": "grunt jasmine-server", 25 | "build": "grunt" 26 | }, 27 | "author": { 28 | "name": "Derick Bailey", 29 | "email": "marionettejs@gmail.com", 30 | "web": "http://derickbailey.lostechies.com" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/marionettejs/backbone.babysitter/issues" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/marionettejs/backbone.babysitter.git" 38 | }, 39 | "github": "https://github.com/marionettejs/backbone.babysitter", 40 | "peerDependencies": { 41 | "backbone": "^1.3.3", 42 | "underscore": "^1.8.3" 43 | }, 44 | "devDependencies": { 45 | "backbone": ">=1.3.3", 46 | "grunt": "0.4.4", 47 | "grunt-cli": "0.1.13", 48 | "grunt-contrib-concat": "0.1.2", 49 | "grunt-contrib-connect": "0.1.2", 50 | "grunt-contrib-jasmine": "^0.9.2", 51 | "grunt-contrib-jshint": "0.1.1", 52 | "grunt-contrib-uglify": "1.0.1", 53 | "grunt-contrib-watch": "0.2.0", 54 | "grunt-preprocess": "4.0.0", 55 | "grunt-template": "0.2.3", 56 | "underscore": ">=1.4.0 <=1.9.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/javascripts/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 1.0.0 2 | 3 | // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | (function(){ 9 | 10 | // Initial Setup 11 | // ------------- 12 | 13 | // Save a reference to the global object (`window` in the browser, `exports` 14 | // on the server). 15 | var root = this; 16 | 17 | // Save the previous value of the `Backbone` variable, so that it can be 18 | // restored later on, if `noConflict` is used. 19 | var previousBackbone = root.Backbone; 20 | 21 | // Create local references to array methods we'll want to use later. 22 | var array = []; 23 | var push = array.push; 24 | var slice = array.slice; 25 | var splice = array.splice; 26 | 27 | // The top-level namespace. All public Backbone classes and modules will 28 | // be attached to this. Exported for both the browser and the server. 29 | var Backbone; 30 | if (typeof exports !== 'undefined') { 31 | Backbone = exports; 32 | } else { 33 | Backbone = root.Backbone = {}; 34 | } 35 | 36 | // Current version of the library. Keep in sync with `package.json`. 37 | Backbone.VERSION = '1.0.0'; 38 | 39 | // Require Underscore, if we're on the server, and it's not already present. 40 | var _ = root._; 41 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); 42 | 43 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 44 | // the `$` variable. 45 | Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; 46 | 47 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 48 | // to its previous owner. Returns a reference to this Backbone object. 49 | Backbone.noConflict = function() { 50 | root.Backbone = previousBackbone; 51 | return this; 52 | }; 53 | 54 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 55 | // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and 56 | // set a `X-Http-Method-Override` header. 57 | Backbone.emulateHTTP = false; 58 | 59 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 60 | // `application/json` requests ... will encode the body as 61 | // `application/x-www-form-urlencoded` instead and will send the model in a 62 | // form param named `model`. 63 | Backbone.emulateJSON = false; 64 | 65 | // Backbone.Events 66 | // --------------- 67 | 68 | // A module that can be mixed in to *any object* in order to provide it with 69 | // custom events. You may bind with `on` or remove with `off` callback 70 | // functions to an event; `trigger`-ing an event fires all callbacks in 71 | // succession. 72 | // 73 | // var object = {}; 74 | // _.extend(object, Backbone.Events); 75 | // object.on('expand', function(){ alert('expanded'); }); 76 | // object.trigger('expand'); 77 | // 78 | var Events = Backbone.Events = { 79 | 80 | // Bind an event to a `callback` function. Passing `"all"` will bind 81 | // the callback to all events fired. 82 | on: function(name, callback, context) { 83 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 84 | this._events || (this._events = {}); 85 | var events = this._events[name] || (this._events[name] = []); 86 | events.push({callback: callback, context: context, ctx: context || this}); 87 | return this; 88 | }, 89 | 90 | // Bind an event to only be triggered a single time. After the first time 91 | // the callback is invoked, it will be removed. 92 | once: function(name, callback, context) { 93 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 94 | var self = this; 95 | var once = _.once(function() { 96 | self.off(name, once); 97 | callback.apply(this, arguments); 98 | }); 99 | once._callback = callback; 100 | return this.on(name, once, context); 101 | }, 102 | 103 | // Remove one or many callbacks. If `context` is null, removes all 104 | // callbacks with that function. If `callback` is null, removes all 105 | // callbacks for the event. If `name` is null, removes all bound 106 | // callbacks for all events. 107 | off: function(name, callback, context) { 108 | var retain, ev, events, names, i, l, j, k; 109 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 110 | if (!name && !callback && !context) { 111 | this._events = {}; 112 | return this; 113 | } 114 | 115 | names = name ? [name] : _.keys(this._events); 116 | for (i = 0, l = names.length; i < l; i++) { 117 | name = names[i]; 118 | if (events = this._events[name]) { 119 | this._events[name] = retain = []; 120 | if (callback || context) { 121 | for (j = 0, k = events.length; j < k; j++) { 122 | ev = events[j]; 123 | if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 124 | (context && context !== ev.context)) { 125 | retain.push(ev); 126 | } 127 | } 128 | } 129 | if (!retain.length) delete this._events[name]; 130 | } 131 | } 132 | 133 | return this; 134 | }, 135 | 136 | // Trigger one or many events, firing all bound callbacks. Callbacks are 137 | // passed the same arguments as `trigger` is, apart from the event name 138 | // (unless you're listening on `"all"`, which will cause your callback to 139 | // receive the true name of the event as the first argument). 140 | trigger: function(name) { 141 | if (!this._events) return this; 142 | var args = slice.call(arguments, 1); 143 | if (!eventsApi(this, 'trigger', name, args)) return this; 144 | var events = this._events[name]; 145 | var allEvents = this._events.all; 146 | if (events) triggerEvents(events, args); 147 | if (allEvents) triggerEvents(allEvents, arguments); 148 | return this; 149 | }, 150 | 151 | // Tell this object to stop listening to either specific events ... or 152 | // to every object it's currently listening to. 153 | stopListening: function(obj, name, callback) { 154 | var listeners = this._listeners; 155 | if (!listeners) return this; 156 | var deleteListener = !name && !callback; 157 | if (typeof name === 'object') callback = this; 158 | if (obj) (listeners = {})[obj._listenerId] = obj; 159 | for (var id in listeners) { 160 | listeners[id].off(name, callback, this); 161 | if (deleteListener) delete this._listeners[id]; 162 | } 163 | return this; 164 | } 165 | 166 | }; 167 | 168 | // Regular expression used to split event strings. 169 | var eventSplitter = /\s+/; 170 | 171 | // Implement fancy features of the Events API such as multiple event 172 | // names `"change blur"` and jQuery-style event maps `{change: action}` 173 | // in terms of the existing API. 174 | var eventsApi = function(obj, action, name, rest) { 175 | if (!name) return true; 176 | 177 | // Handle event maps. 178 | if (typeof name === 'object') { 179 | for (var key in name) { 180 | obj[action].apply(obj, [key, name[key]].concat(rest)); 181 | } 182 | return false; 183 | } 184 | 185 | // Handle space separated event names. 186 | if (eventSplitter.test(name)) { 187 | var names = name.split(eventSplitter); 188 | for (var i = 0, l = names.length; i < l; i++) { 189 | obj[action].apply(obj, [names[i]].concat(rest)); 190 | } 191 | return false; 192 | } 193 | 194 | return true; 195 | }; 196 | 197 | // A difficult-to-believe, but optimized internal dispatch function for 198 | // triggering events. Tries to keep the usual cases speedy (most internal 199 | // Backbone events have 3 arguments). 200 | var triggerEvents = function(events, args) { 201 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 202 | switch (args.length) { 203 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 204 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 205 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 206 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 207 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); 208 | } 209 | }; 210 | 211 | var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 212 | 213 | // Inversion-of-control versions of `on` and `once`. Tell *this* object to 214 | // listen to an event in another object ... keeping track of what it's 215 | // listening to. 216 | _.each(listenMethods, function(implementation, method) { 217 | Events[method] = function(obj, name, callback) { 218 | var listeners = this._listeners || (this._listeners = {}); 219 | var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); 220 | listeners[id] = obj; 221 | if (typeof name === 'object') callback = this; 222 | obj[implementation](name, callback, this); 223 | return this; 224 | }; 225 | }); 226 | 227 | // Aliases for backwards compatibility. 228 | Events.bind = Events.on; 229 | Events.unbind = Events.off; 230 | 231 | // Allow the `Backbone` object to serve as a global event bus, for folks who 232 | // want global "pubsub" in a convenient place. 233 | _.extend(Backbone, Events); 234 | 235 | // Backbone.Model 236 | // -------------- 237 | 238 | // Backbone **Models** are the basic data object in the framework -- 239 | // frequently representing a row in a table in a database on your server. 240 | // A discrete chunk of data and a bunch of useful, related methods for 241 | // performing computations and transformations on that data. 242 | 243 | // Create a new model with the specified attributes. A client id (`cid`) 244 | // is automatically generated and assigned for you. 245 | var Model = Backbone.Model = function(attributes, options) { 246 | var defaults; 247 | var attrs = attributes || {}; 248 | options || (options = {}); 249 | this.cid = _.uniqueId('c'); 250 | this.attributes = {}; 251 | _.extend(this, _.pick(options, modelOptions)); 252 | if (options.parse) attrs = this.parse(attrs, options) || {}; 253 | if (defaults = _.result(this, 'defaults')) { 254 | attrs = _.defaults({}, attrs, defaults); 255 | } 256 | this.set(attrs, options); 257 | this.changed = {}; 258 | this.initialize.apply(this, arguments); 259 | }; 260 | 261 | // A list of options to be attached directly to the model, if provided. 262 | var modelOptions = ['url', 'urlRoot', 'collection']; 263 | 264 | // Attach all inheritable methods to the Model prototype. 265 | _.extend(Model.prototype, Events, { 266 | 267 | // A hash of attributes whose current and previous value differ. 268 | changed: null, 269 | 270 | // The value returned during the last failed validation. 271 | validationError: null, 272 | 273 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 274 | // CouchDB users may want to set this to `"_id"`. 275 | idAttribute: 'id', 276 | 277 | // Initialize is an empty function by default. Override it with your own 278 | // initialization logic. 279 | initialize: function(){}, 280 | 281 | // Return a copy of the model's `attributes` object. 282 | toJSON: function(options) { 283 | return _.clone(this.attributes); 284 | }, 285 | 286 | // Proxy `Backbone.sync` by default -- but override this if you need 287 | // custom syncing semantics for *this* particular model. 288 | sync: function() { 289 | return Backbone.sync.apply(this, arguments); 290 | }, 291 | 292 | // Get the value of an attribute. 293 | get: function(attr) { 294 | return this.attributes[attr]; 295 | }, 296 | 297 | // Get the HTML-escaped value of an attribute. 298 | escape: function(attr) { 299 | return _.escape(this.get(attr)); 300 | }, 301 | 302 | // Returns `true` if the attribute contains a value that is not null 303 | // or undefined. 304 | has: function(attr) { 305 | return this.get(attr) != null; 306 | }, 307 | 308 | // Set a hash of model attributes on the object, firing `"change"`. This is 309 | // the core primitive operation of a model, updating the data and notifying 310 | // anyone who needs to know about the change in state. The heart of the beast. 311 | set: function(key, val, options) { 312 | var attr, attrs, unset, changes, silent, changing, prev, current; 313 | if (key == null) return this; 314 | 315 | // Handle both `"key", value` and `{key: value}` -style arguments. 316 | if (typeof key === 'object') { 317 | attrs = key; 318 | options = val; 319 | } else { 320 | (attrs = {})[key] = val; 321 | } 322 | 323 | options || (options = {}); 324 | 325 | // Run validation. 326 | if (!this._validate(attrs, options)) return false; 327 | 328 | // Extract attributes and options. 329 | unset = options.unset; 330 | silent = options.silent; 331 | changes = []; 332 | changing = this._changing; 333 | this._changing = true; 334 | 335 | if (!changing) { 336 | this._previousAttributes = _.clone(this.attributes); 337 | this.changed = {}; 338 | } 339 | current = this.attributes, prev = this._previousAttributes; 340 | 341 | // Check for changes of `id`. 342 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 343 | 344 | // For each `set` attribute, update or delete the current value. 345 | for (attr in attrs) { 346 | val = attrs[attr]; 347 | if (!_.isEqual(current[attr], val)) changes.push(attr); 348 | if (!_.isEqual(prev[attr], val)) { 349 | this.changed[attr] = val; 350 | } else { 351 | delete this.changed[attr]; 352 | } 353 | unset ? delete current[attr] : current[attr] = val; 354 | } 355 | 356 | // Trigger all relevant attribute changes. 357 | if (!silent) { 358 | if (changes.length) this._pending = true; 359 | for (var i = 0, l = changes.length; i < l; i++) { 360 | this.trigger('change:' + changes[i], this, current[changes[i]], options); 361 | } 362 | } 363 | 364 | // You might be wondering why there's a `while` loop here. Changes can 365 | // be recursively nested within `"change"` events. 366 | if (changing) return this; 367 | if (!silent) { 368 | while (this._pending) { 369 | this._pending = false; 370 | this.trigger('change', this, options); 371 | } 372 | } 373 | this._pending = false; 374 | this._changing = false; 375 | return this; 376 | }, 377 | 378 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop 379 | // if the attribute doesn't exist. 380 | unset: function(attr, options) { 381 | return this.set(attr, void 0, _.extend({}, options, {unset: true})); 382 | }, 383 | 384 | // Clear all attributes on the model, firing `"change"`. 385 | clear: function(options) { 386 | var attrs = {}; 387 | for (var key in this.attributes) attrs[key] = void 0; 388 | return this.set(attrs, _.extend({}, options, {unset: true})); 389 | }, 390 | 391 | // Determine if the model has changed since the last `"change"` event. 392 | // If you specify an attribute name, determine if that attribute has changed. 393 | hasChanged: function(attr) { 394 | if (attr == null) return !_.isEmpty(this.changed); 395 | return _.has(this.changed, attr); 396 | }, 397 | 398 | // Return an object containing all the attributes that have changed, or 399 | // false if there are no changed attributes. Useful for determining what 400 | // parts of a view need to be updated and/or what attributes need to be 401 | // persisted to the server. Unset attributes will be set to undefined. 402 | // You can also pass an attributes object to diff against the model, 403 | // determining if there *would be* a change. 404 | changedAttributes: function(diff) { 405 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 406 | var val, changed = false; 407 | var old = this._changing ? this._previousAttributes : this.attributes; 408 | for (var attr in diff) { 409 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 410 | (changed || (changed = {}))[attr] = val; 411 | } 412 | return changed; 413 | }, 414 | 415 | // Get the previous value of an attribute, recorded at the time the last 416 | // `"change"` event was fired. 417 | previous: function(attr) { 418 | if (attr == null || !this._previousAttributes) return null; 419 | return this._previousAttributes[attr]; 420 | }, 421 | 422 | // Get all of the attributes of the model at the time of the previous 423 | // `"change"` event. 424 | previousAttributes: function() { 425 | return _.clone(this._previousAttributes); 426 | }, 427 | 428 | // Fetch the model from the server. If the server's representation of the 429 | // model differs from its current attributes, they will be overridden, 430 | // triggering a `"change"` event. 431 | fetch: function(options) { 432 | options = options ? _.clone(options) : {}; 433 | if (options.parse === void 0) options.parse = true; 434 | var model = this; 435 | var success = options.success; 436 | options.success = function(resp) { 437 | if (!model.set(model.parse(resp, options), options)) return false; 438 | if (success) success(model, resp, options); 439 | model.trigger('sync', model, resp, options); 440 | }; 441 | wrapError(this, options); 442 | return this.sync('read', this, options); 443 | }, 444 | 445 | // Set a hash of model attributes, and sync the model to the server. 446 | // If the server returns an attributes hash that differs, the model's 447 | // state will be `set` again. 448 | save: function(key, val, options) { 449 | var attrs, method, xhr, attributes = this.attributes; 450 | 451 | // Handle both `"key", value` and `{key: value}` -style arguments. 452 | if (key == null || typeof key === 'object') { 453 | attrs = key; 454 | options = val; 455 | } else { 456 | (attrs = {})[key] = val; 457 | } 458 | 459 | // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. 460 | if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; 461 | 462 | options = _.extend({validate: true}, options); 463 | 464 | // Do not persist invalid models. 465 | if (!this._validate(attrs, options)) return false; 466 | 467 | // Set temporary attributes if `{wait: true}`. 468 | if (attrs && options.wait) { 469 | this.attributes = _.extend({}, attributes, attrs); 470 | } 471 | 472 | // After a successful server-side save, the client is (optionally) 473 | // updated with the server-side state. 474 | if (options.parse === void 0) options.parse = true; 475 | var model = this; 476 | var success = options.success; 477 | options.success = function(resp) { 478 | // Ensure attributes are restored during synchronous saves. 479 | model.attributes = attributes; 480 | var serverAttrs = model.parse(resp, options); 481 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 482 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 483 | return false; 484 | } 485 | if (success) success(model, resp, options); 486 | model.trigger('sync', model, resp, options); 487 | }; 488 | wrapError(this, options); 489 | 490 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 491 | if (method === 'patch') options.attrs = attrs; 492 | xhr = this.sync(method, this, options); 493 | 494 | // Restore attributes. 495 | if (attrs && options.wait) this.attributes = attributes; 496 | 497 | return xhr; 498 | }, 499 | 500 | // Destroy this model on the server if it was already persisted. 501 | // Optimistically removes the model from its collection, if it has one. 502 | // If `wait: true` is passed, waits for the server to respond before removal. 503 | destroy: function(options) { 504 | options = options ? _.clone(options) : {}; 505 | var model = this; 506 | var success = options.success; 507 | 508 | var destroy = function() { 509 | model.trigger('destroy', model, model.collection, options); 510 | }; 511 | 512 | options.success = function(resp) { 513 | if (options.wait || model.isNew()) destroy(); 514 | if (success) success(model, resp, options); 515 | if (!model.isNew()) model.trigger('sync', model, resp, options); 516 | }; 517 | 518 | if (this.isNew()) { 519 | options.success(); 520 | return false; 521 | } 522 | wrapError(this, options); 523 | 524 | var xhr = this.sync('delete', this, options); 525 | if (!options.wait) destroy(); 526 | return xhr; 527 | }, 528 | 529 | // Default URL for the model's representation on the server -- if you're 530 | // using Backbone's restful methods, override this to change the endpoint 531 | // that will be called. 532 | url: function() { 533 | var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); 534 | if (this.isNew()) return base; 535 | return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); 536 | }, 537 | 538 | // **parse** converts a response into the hash of attributes to be `set` on 539 | // the model. The default implementation is just to pass the response along. 540 | parse: function(resp, options) { 541 | return resp; 542 | }, 543 | 544 | // Create a new model with identical attributes to this one. 545 | clone: function() { 546 | return new this.constructor(this.attributes); 547 | }, 548 | 549 | // A model is new if it has never been saved to the server, and lacks an id. 550 | isNew: function() { 551 | return this.id == null; 552 | }, 553 | 554 | // Check if the model is currently in a valid state. 555 | isValid: function(options) { 556 | return this._validate({}, _.extend(options || {}, { validate: true })); 557 | }, 558 | 559 | // Run validation against the next complete set of model attributes, 560 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 561 | _validate: function(attrs, options) { 562 | if (!options.validate || !this.validate) return true; 563 | attrs = _.extend({}, this.attributes, attrs); 564 | var error = this.validationError = this.validate(attrs, options) || null; 565 | if (!error) return true; 566 | this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); 567 | return false; 568 | } 569 | 570 | }); 571 | 572 | // Underscore methods that we want to implement on the Model. 573 | var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; 574 | 575 | // Mix in each Underscore method as a proxy to `Model#attributes`. 576 | _.each(modelMethods, function(method) { 577 | Model.prototype[method] = function() { 578 | var args = slice.call(arguments); 579 | args.unshift(this.attributes); 580 | return _[method].apply(_, args); 581 | }; 582 | }); 583 | 584 | // Backbone.Collection 585 | // ------------------- 586 | 587 | // If models tend to represent a single row of data, a Backbone Collection is 588 | // more analagous to a table full of data ... or a small slice or page of that 589 | // table, or a collection of rows that belong together for a particular reason 590 | // -- all of the messages in this particular folder, all of the documents 591 | // belonging to this particular author, and so on. Collections maintain 592 | // indexes of their models, both in order, and for lookup by `id`. 593 | 594 | // Create a new **Collection**, perhaps to contain a specific type of `model`. 595 | // If a `comparator` is specified, the Collection will maintain 596 | // its models in sort order, as they're added and removed. 597 | var Collection = Backbone.Collection = function(models, options) { 598 | options || (options = {}); 599 | if (options.url) this.url = options.url; 600 | if (options.model) this.model = options.model; 601 | if (options.comparator !== void 0) this.comparator = options.comparator; 602 | this._reset(); 603 | this.initialize.apply(this, arguments); 604 | if (models) this.reset(models, _.extend({silent: true}, options)); 605 | }; 606 | 607 | // Default options for `Collection#set`. 608 | var setOptions = {add: true, remove: true, merge: true}; 609 | var addOptions = {add: true, merge: false, remove: false}; 610 | 611 | // Define the Collection's inheritable methods. 612 | _.extend(Collection.prototype, Events, { 613 | 614 | // The default model for a collection is just a **Backbone.Model**. 615 | // This should be overridden in most cases. 616 | model: Model, 617 | 618 | // Initialize is an empty function by default. Override it with your own 619 | // initialization logic. 620 | initialize: function(){}, 621 | 622 | // The JSON representation of a Collection is an array of the 623 | // models' attributes. 624 | toJSON: function(options) { 625 | return this.map(function(model){ return model.toJSON(options); }); 626 | }, 627 | 628 | // Proxy `Backbone.sync` by default. 629 | sync: function() { 630 | return Backbone.sync.apply(this, arguments); 631 | }, 632 | 633 | // Add a model, or list of models to the set. 634 | add: function(models, options) { 635 | return this.set(models, _.defaults(options || {}, addOptions)); 636 | }, 637 | 638 | // Remove a model, or a list of models from the set. 639 | remove: function(models, options) { 640 | models = _.isArray(models) ? models.slice() : [models]; 641 | options || (options = {}); 642 | var i, l, index, model; 643 | for (i = 0, l = models.length; i < l; i++) { 644 | model = this.get(models[i]); 645 | if (!model) continue; 646 | delete this._byId[model.id]; 647 | delete this._byId[model.cid]; 648 | index = this.indexOf(model); 649 | this.models.splice(index, 1); 650 | this.length--; 651 | if (!options.silent) { 652 | options.index = index; 653 | model.trigger('remove', model, this, options); 654 | } 655 | this._removeReference(model); 656 | } 657 | return this; 658 | }, 659 | 660 | // Update a collection by `set`-ing a new list of models, adding new ones, 661 | // removing models that are no longer present, and merging models that 662 | // already exist in the collection, as necessary. Similar to **Model#set**, 663 | // the core operation for updating the data contained by the collection. 664 | set: function(models, options) { 665 | options = _.defaults(options || {}, setOptions); 666 | if (options.parse) models = this.parse(models, options); 667 | if (!_.isArray(models)) models = models ? [models] : []; 668 | var i, l, model, attrs, existing, sort; 669 | var at = options.at; 670 | var sortable = this.comparator && (at == null) && options.sort !== false; 671 | var sortAttr = _.isString(this.comparator) ? this.comparator : null; 672 | var toAdd = [], toRemove = [], modelMap = {}; 673 | 674 | // Turn bare objects into model references, and prevent invalid models 675 | // from being added. 676 | for (i = 0, l = models.length; i < l; i++) { 677 | if (!(model = this._prepareModel(models[i], options))) continue; 678 | 679 | // If a duplicate is found, prevent it from being added and 680 | // optionally merge it into the existing model. 681 | if (existing = this.get(model)) { 682 | if (options.remove) modelMap[existing.cid] = true; 683 | if (options.merge) { 684 | existing.set(model.attributes, options); 685 | if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 686 | } 687 | 688 | // This is a new model, push it to the `toAdd` list. 689 | } else if (options.add) { 690 | toAdd.push(model); 691 | 692 | // Listen to added models' events, and index models for lookup by 693 | // `id` and by `cid`. 694 | model.on('all', this._onModelEvent, this); 695 | this._byId[model.cid] = model; 696 | if (model.id != null) this._byId[model.id] = model; 697 | } 698 | } 699 | 700 | // Remove nonexistent models if appropriate. 701 | if (options.remove) { 702 | for (i = 0, l = this.length; i < l; ++i) { 703 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 704 | } 705 | if (toRemove.length) this.remove(toRemove, options); 706 | } 707 | 708 | // See if sorting is needed, update `length` and splice in new models. 709 | if (toAdd.length) { 710 | if (sortable) sort = true; 711 | this.length += toAdd.length; 712 | if (at != null) { 713 | splice.apply(this.models, [at, 0].concat(toAdd)); 714 | } else { 715 | push.apply(this.models, toAdd); 716 | } 717 | } 718 | 719 | // Silently sort the collection if appropriate. 720 | if (sort) this.sort({silent: true}); 721 | 722 | if (options.silent) return this; 723 | 724 | // Trigger `add` events. 725 | for (i = 0, l = toAdd.length; i < l; i++) { 726 | (model = toAdd[i]).trigger('add', model, this, options); 727 | } 728 | 729 | // Trigger `sort` if the collection was sorted. 730 | if (sort) this.trigger('sort', this, options); 731 | return this; 732 | }, 733 | 734 | // When you have more items than you want to add or remove individually, 735 | // you can reset the entire set with a new list of models, without firing 736 | // any granular `add` or `remove` events. Fires `reset` when finished. 737 | // Useful for bulk operations and optimizations. 738 | reset: function(models, options) { 739 | options || (options = {}); 740 | for (var i = 0, l = this.models.length; i < l; i++) { 741 | this._removeReference(this.models[i]); 742 | } 743 | options.previousModels = this.models; 744 | this._reset(); 745 | this.add(models, _.extend({silent: true}, options)); 746 | if (!options.silent) this.trigger('reset', this, options); 747 | return this; 748 | }, 749 | 750 | // Add a model to the end of the collection. 751 | push: function(model, options) { 752 | model = this._prepareModel(model, options); 753 | this.add(model, _.extend({at: this.length}, options)); 754 | return model; 755 | }, 756 | 757 | // Remove a model from the end of the collection. 758 | pop: function(options) { 759 | var model = this.at(this.length - 1); 760 | this.remove(model, options); 761 | return model; 762 | }, 763 | 764 | // Add a model to the beginning of the collection. 765 | unshift: function(model, options) { 766 | model = this._prepareModel(model, options); 767 | this.add(model, _.extend({at: 0}, options)); 768 | return model; 769 | }, 770 | 771 | // Remove a model from the beginning of the collection. 772 | shift: function(options) { 773 | var model = this.at(0); 774 | this.remove(model, options); 775 | return model; 776 | }, 777 | 778 | // Slice out a sub-array of models from the collection. 779 | slice: function(begin, end) { 780 | return this.models.slice(begin, end); 781 | }, 782 | 783 | // Get a model from the set by id. 784 | get: function(obj) { 785 | if (obj == null) return void 0; 786 | return this._byId[obj.id != null ? obj.id : obj.cid || obj]; 787 | }, 788 | 789 | // Get the model at the given index. 790 | at: function(index) { 791 | return this.models[index]; 792 | }, 793 | 794 | // Return models with matching attributes. Useful for simple cases of 795 | // `filter`. 796 | where: function(attrs, first) { 797 | if (_.isEmpty(attrs)) return first ? void 0 : []; 798 | return this[first ? 'find' : 'filter'](function(model) { 799 | for (var key in attrs) { 800 | if (attrs[key] !== model.get(key)) return false; 801 | } 802 | return true; 803 | }); 804 | }, 805 | 806 | // Return the first model with matching attributes. Useful for simple cases 807 | // of `find`. 808 | findWhere: function(attrs) { 809 | return this.where(attrs, true); 810 | }, 811 | 812 | // Force the collection to re-sort itself. You don't need to call this under 813 | // normal circumstances, as the set will maintain sort order as each item 814 | // is added. 815 | sort: function(options) { 816 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 817 | options || (options = {}); 818 | 819 | // Run sort based on type of `comparator`. 820 | if (_.isString(this.comparator) || this.comparator.length === 1) { 821 | this.models = this.sortBy(this.comparator, this); 822 | } else { 823 | this.models.sort(_.bind(this.comparator, this)); 824 | } 825 | 826 | if (!options.silent) this.trigger('sort', this, options); 827 | return this; 828 | }, 829 | 830 | // Figure out the smallest index at which a model should be inserted so as 831 | // to maintain order. 832 | sortedIndex: function(model, value, context) { 833 | value || (value = this.comparator); 834 | var iterator = _.isFunction(value) ? value : function(model) { 835 | return model.get(value); 836 | }; 837 | return _.sortedIndex(this.models, model, iterator, context); 838 | }, 839 | 840 | // Pluck an attribute from each model in the collection. 841 | pluck: function(attr) { 842 | return _.invoke(this.models, 'get', attr); 843 | }, 844 | 845 | // Fetch the default set of models for this collection, resetting the 846 | // collection when they arrive. If `reset: true` is passed, the response 847 | // data will be passed through the `reset` method instead of `set`. 848 | fetch: function(options) { 849 | options = options ? _.clone(options) : {}; 850 | if (options.parse === void 0) options.parse = true; 851 | var success = options.success; 852 | var collection = this; 853 | options.success = function(resp) { 854 | var method = options.reset ? 'reset' : 'set'; 855 | collection[method](resp, options); 856 | if (success) success(collection, resp, options); 857 | collection.trigger('sync', collection, resp, options); 858 | }; 859 | wrapError(this, options); 860 | return this.sync('read', this, options); 861 | }, 862 | 863 | // Create a new instance of a model in this collection. Add the model to the 864 | // collection immediately, unless `wait: true` is passed, in which case we 865 | // wait for the server to agree. 866 | create: function(model, options) { 867 | options = options ? _.clone(options) : {}; 868 | if (!(model = this._prepareModel(model, options))) return false; 869 | if (!options.wait) this.add(model, options); 870 | var collection = this; 871 | var success = options.success; 872 | options.success = function(resp) { 873 | if (options.wait) collection.add(model, options); 874 | if (success) success(model, resp, options); 875 | }; 876 | model.save(null, options); 877 | return model; 878 | }, 879 | 880 | // **parse** converts a response into a list of models to be added to the 881 | // collection. The default implementation is just to pass it through. 882 | parse: function(resp, options) { 883 | return resp; 884 | }, 885 | 886 | // Create a new collection with an identical list of models as this one. 887 | clone: function() { 888 | return new this.constructor(this.models); 889 | }, 890 | 891 | // Private method to reset all internal state. Called when the collection 892 | // is first initialized or reset. 893 | _reset: function() { 894 | this.length = 0; 895 | this.models = []; 896 | this._byId = {}; 897 | }, 898 | 899 | // Prepare a hash of attributes (or other model) to be added to this 900 | // collection. 901 | _prepareModel: function(attrs, options) { 902 | if (attrs instanceof Model) { 903 | if (!attrs.collection) attrs.collection = this; 904 | return attrs; 905 | } 906 | options || (options = {}); 907 | options.collection = this; 908 | var model = new this.model(attrs, options); 909 | if (!model._validate(attrs, options)) { 910 | this.trigger('invalid', this, attrs, options); 911 | return false; 912 | } 913 | return model; 914 | }, 915 | 916 | // Internal method to sever a model's ties to a collection. 917 | _removeReference: function(model) { 918 | if (this === model.collection) delete model.collection; 919 | model.off('all', this._onModelEvent, this); 920 | }, 921 | 922 | // Internal method called every time a model in the set fires an event. 923 | // Sets need to update their indexes when models change ids. All other 924 | // events simply proxy through. "add" and "remove" events that originate 925 | // in other collections are ignored. 926 | _onModelEvent: function(event, model, collection, options) { 927 | if ((event === 'add' || event === 'remove') && collection !== this) return; 928 | if (event === 'destroy') this.remove(model, options); 929 | if (model && event === 'change:' + model.idAttribute) { 930 | delete this._byId[model.previous(model.idAttribute)]; 931 | if (model.id != null) this._byId[model.id] = model; 932 | } 933 | this.trigger.apply(this, arguments); 934 | } 935 | 936 | }); 937 | 938 | // Underscore methods that we want to implement on the Collection. 939 | // 90% of the core usefulness of Backbone Collections is actually implemented 940 | // right here: 941 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 942 | 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 943 | 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 944 | 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 945 | 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 946 | 'isEmpty', 'chain']; 947 | 948 | // Mix in each Underscore method as a proxy to `Collection#models`. 949 | _.each(methods, function(method) { 950 | Collection.prototype[method] = function() { 951 | var args = slice.call(arguments); 952 | args.unshift(this.models); 953 | return _[method].apply(_, args); 954 | }; 955 | }); 956 | 957 | // Underscore methods that take a property name as an argument. 958 | var attributeMethods = ['groupBy', 'countBy', 'sortBy']; 959 | 960 | // Use attributes instead of properties. 961 | _.each(attributeMethods, function(method) { 962 | Collection.prototype[method] = function(value, context) { 963 | var iterator = _.isFunction(value) ? value : function(model) { 964 | return model.get(value); 965 | }; 966 | return _[method](this.models, iterator, context); 967 | }; 968 | }); 969 | 970 | // Backbone.View 971 | // ------------- 972 | 973 | // Backbone Views are almost more convention than they are actual code. A View 974 | // is simply a JavaScript object that represents a logical chunk of UI in the 975 | // DOM. This might be a single item, an entire list, a sidebar or panel, or 976 | // even the surrounding frame which wraps your whole app. Defining a chunk of 977 | // UI as a **View** allows you to define your DOM events declaratively, without 978 | // having to worry about render order ... and makes it easy for the view to 979 | // react to specific changes in the state of your models. 980 | 981 | // Creating a Backbone.View creates its initial element outside of the DOM, 982 | // if an existing element is not provided... 983 | var View = Backbone.View = function(options) { 984 | this.cid = _.uniqueId('view'); 985 | this._configure(options || {}); 986 | this._ensureElement(); 987 | this.initialize.apply(this, arguments); 988 | this.delegateEvents(); 989 | }; 990 | 991 | // Cached regex to split keys for `delegate`. 992 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 993 | 994 | // List of view options to be merged as properties. 995 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 996 | 997 | // Set up all inheritable **Backbone.View** properties and methods. 998 | _.extend(View.prototype, Events, { 999 | 1000 | // The default `tagName` of a View's element is `"div"`. 1001 | tagName: 'div', 1002 | 1003 | // jQuery delegate for element lookup, scoped to DOM elements within the 1004 | // current view. This should be prefered to global lookups where possible. 1005 | $: function(selector) { 1006 | return this.$el.find(selector); 1007 | }, 1008 | 1009 | // Initialize is an empty function by default. Override it with your own 1010 | // initialization logic. 1011 | initialize: function(){}, 1012 | 1013 | // **render** is the core function that your view should override, in order 1014 | // to populate its element (`this.el`), with the appropriate HTML. The 1015 | // convention is for **render** to always return `this`. 1016 | render: function() { 1017 | return this; 1018 | }, 1019 | 1020 | // Remove this view by taking the element out of the DOM, and removing any 1021 | // applicable Backbone.Events listeners. 1022 | remove: function() { 1023 | this.$el.remove(); 1024 | this.stopListening(); 1025 | return this; 1026 | }, 1027 | 1028 | // Change the view's element (`this.el` property), including event 1029 | // re-delegation. 1030 | setElement: function(element, delegate) { 1031 | if (this.$el) this.undelegateEvents(); 1032 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); 1033 | this.el = this.$el[0]; 1034 | if (delegate !== false) this.delegateEvents(); 1035 | return this; 1036 | }, 1037 | 1038 | // Set callbacks, where `this.events` is a hash of 1039 | // 1040 | // *{"event selector": "callback"}* 1041 | // 1042 | // { 1043 | // 'mousedown .title': 'edit', 1044 | // 'click .button': 'save' 1045 | // 'click .open': function(e) { ... } 1046 | // } 1047 | // 1048 | // pairs. Callbacks will be bound to the view, with `this` set properly. 1049 | // Uses event delegation for efficiency. 1050 | // Omitting the selector binds the event to `this.el`. 1051 | // This only works for delegate-able events: not `focus`, `blur`, and 1052 | // not `change`, `submit`, and `reset` in Internet Explorer. 1053 | delegateEvents: function(events) { 1054 | if (!(events || (events = _.result(this, 'events')))) return this; 1055 | this.undelegateEvents(); 1056 | for (var key in events) { 1057 | var method = events[key]; 1058 | if (!_.isFunction(method)) method = this[events[key]]; 1059 | if (!method) continue; 1060 | 1061 | var match = key.match(delegateEventSplitter); 1062 | var eventName = match[1], selector = match[2]; 1063 | method = _.bind(method, this); 1064 | eventName += '.delegateEvents' + this.cid; 1065 | if (selector === '') { 1066 | this.$el.on(eventName, method); 1067 | } else { 1068 | this.$el.on(eventName, selector, method); 1069 | } 1070 | } 1071 | return this; 1072 | }, 1073 | 1074 | // Clears all callbacks previously bound to the view with `delegateEvents`. 1075 | // You usually don't need to use this, but may wish to if you have multiple 1076 | // Backbone views attached to the same DOM element. 1077 | undelegateEvents: function() { 1078 | this.$el.off('.delegateEvents' + this.cid); 1079 | return this; 1080 | }, 1081 | 1082 | // Performs the initial configuration of a View with a set of options. 1083 | // Keys with special meaning *(e.g. model, collection, id, className)* are 1084 | // attached directly to the view. See `viewOptions` for an exhaustive 1085 | // list. 1086 | _configure: function(options) { 1087 | if (this.options) options = _.extend({}, _.result(this, 'options'), options); 1088 | _.extend(this, _.pick(options, viewOptions)); 1089 | this.options = options; 1090 | }, 1091 | 1092 | // Ensure that the View has a DOM element to render into. 1093 | // If `this.el` is a string, pass it through `$()`, take the first 1094 | // matching element, and re-assign it to `el`. Otherwise, create 1095 | // an element from the `id`, `className` and `tagName` properties. 1096 | _ensureElement: function() { 1097 | if (!this.el) { 1098 | var attrs = _.extend({}, _.result(this, 'attributes')); 1099 | if (this.id) attrs.id = _.result(this, 'id'); 1100 | if (this.className) attrs['class'] = _.result(this, 'className'); 1101 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); 1102 | this.setElement($el, false); 1103 | } else { 1104 | this.setElement(_.result(this, 'el'), false); 1105 | } 1106 | } 1107 | 1108 | }); 1109 | 1110 | // Backbone.sync 1111 | // ------------- 1112 | 1113 | // Override this function to change the manner in which Backbone persists 1114 | // models to the server. You will be passed the type of request, and the 1115 | // model in question. By default, makes a RESTful Ajax request 1116 | // to the model's `url()`. Some possible customizations could be: 1117 | // 1118 | // * Use `setTimeout` to batch rapid-fire updates into a single request. 1119 | // * Send up the models as XML instead of JSON. 1120 | // * Persist models via WebSockets instead of Ajax. 1121 | // 1122 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1123 | // as `POST`, with a `_method` parameter containing the true HTTP method, 1124 | // as well as all requests with the body as `application/x-www-form-urlencoded` 1125 | // instead of `application/json` with the model in a param named `model`. 1126 | // Useful when interfacing with server-side languages like **PHP** that make 1127 | // it difficult to read the body of `PUT` requests. 1128 | Backbone.sync = function(method, model, options) { 1129 | var type = methodMap[method]; 1130 | 1131 | // Default options, unless specified. 1132 | _.defaults(options || (options = {}), { 1133 | emulateHTTP: Backbone.emulateHTTP, 1134 | emulateJSON: Backbone.emulateJSON 1135 | }); 1136 | 1137 | // Default JSON-request options. 1138 | var params = {type: type, dataType: 'json'}; 1139 | 1140 | // Ensure that we have a URL. 1141 | if (!options.url) { 1142 | params.url = _.result(model, 'url') || urlError(); 1143 | } 1144 | 1145 | // Ensure that we have the appropriate request data. 1146 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1147 | params.contentType = 'application/json'; 1148 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1149 | } 1150 | 1151 | // For older servers, emulate JSON by encoding the request into an HTML-form. 1152 | if (options.emulateJSON) { 1153 | params.contentType = 'application/x-www-form-urlencoded'; 1154 | params.data = params.data ? {model: params.data} : {}; 1155 | } 1156 | 1157 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1158 | // And an `X-HTTP-Method-Override` header. 1159 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1160 | params.type = 'POST'; 1161 | if (options.emulateJSON) params.data._method = type; 1162 | var beforeSend = options.beforeSend; 1163 | options.beforeSend = function(xhr) { 1164 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 1165 | if (beforeSend) return beforeSend.apply(this, arguments); 1166 | }; 1167 | } 1168 | 1169 | // Don't process data on a non-GET request. 1170 | if (params.type !== 'GET' && !options.emulateJSON) { 1171 | params.processData = false; 1172 | } 1173 | 1174 | // If we're sending a `PATCH` request, and we're in an old Internet Explorer 1175 | // that still has ActiveX enabled by default, override jQuery to use that 1176 | // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. 1177 | if (params.type === 'PATCH' && window.ActiveXObject && 1178 | !(window.external && window.external.msActiveXFilteringEnabled)) { 1179 | params.xhr = function() { 1180 | return new ActiveXObject("Microsoft.XMLHTTP"); 1181 | }; 1182 | } 1183 | 1184 | // Make the request, allowing the user to override any Ajax options. 1185 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1186 | model.trigger('request', model, xhr, options); 1187 | return xhr; 1188 | }; 1189 | 1190 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1191 | var methodMap = { 1192 | 'create': 'POST', 1193 | 'update': 'PUT', 1194 | 'patch': 'PATCH', 1195 | 'delete': 'DELETE', 1196 | 'read': 'GET' 1197 | }; 1198 | 1199 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1200 | // Override this if you'd like to use a different library. 1201 | Backbone.ajax = function() { 1202 | return Backbone.$.ajax.apply(Backbone.$, arguments); 1203 | }; 1204 | 1205 | // Backbone.Router 1206 | // --------------- 1207 | 1208 | // Routers map faux-URLs to actions, and fire events when routes are 1209 | // matched. Creating a new one sets its `routes` hash, if not set statically. 1210 | var Router = Backbone.Router = function(options) { 1211 | options || (options = {}); 1212 | if (options.routes) this.routes = options.routes; 1213 | this._bindRoutes(); 1214 | this.initialize.apply(this, arguments); 1215 | }; 1216 | 1217 | // Cached regular expressions for matching named param parts and splatted 1218 | // parts of route strings. 1219 | var optionalParam = /\((.*?)\)/g; 1220 | var namedParam = /(\(\?)?:\w+/g; 1221 | var splatParam = /\*\w+/g; 1222 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1223 | 1224 | // Set up all inheritable **Backbone.Router** properties and methods. 1225 | _.extend(Router.prototype, Events, { 1226 | 1227 | // Initialize is an empty function by default. Override it with your own 1228 | // initialization logic. 1229 | initialize: function(){}, 1230 | 1231 | // Manually bind a single named route to a callback. For example: 1232 | // 1233 | // this.route('search/:query/p:num', 'search', function(query, num) { 1234 | // ... 1235 | // }); 1236 | // 1237 | route: function(route, name, callback) { 1238 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1239 | if (_.isFunction(name)) { 1240 | callback = name; 1241 | name = ''; 1242 | } 1243 | if (!callback) callback = this[name]; 1244 | var router = this; 1245 | Backbone.history.route(route, function(fragment) { 1246 | var args = router._extractParameters(route, fragment); 1247 | callback && callback.apply(router, args); 1248 | router.trigger.apply(router, ['route:' + name].concat(args)); 1249 | router.trigger('route', name, args); 1250 | Backbone.history.trigger('route', router, name, args); 1251 | }); 1252 | return this; 1253 | }, 1254 | 1255 | // Simple proxy to `Backbone.history` to save a fragment into the history. 1256 | navigate: function(fragment, options) { 1257 | Backbone.history.navigate(fragment, options); 1258 | return this; 1259 | }, 1260 | 1261 | // Bind all defined routes to `Backbone.history`. We have to reverse the 1262 | // order of the routes here to support behavior where the most general 1263 | // routes can be defined at the bottom of the route map. 1264 | _bindRoutes: function() { 1265 | if (!this.routes) return; 1266 | this.routes = _.result(this, 'routes'); 1267 | var route, routes = _.keys(this.routes); 1268 | while ((route = routes.pop()) != null) { 1269 | this.route(route, this.routes[route]); 1270 | } 1271 | }, 1272 | 1273 | // Convert a route string into a regular expression, suitable for matching 1274 | // against the current location hash. 1275 | _routeToRegExp: function(route) { 1276 | route = route.replace(escapeRegExp, '\\$&') 1277 | .replace(optionalParam, '(?:$1)?') 1278 | .replace(namedParam, function(match, optional){ 1279 | return optional ? match : '([^\/]+)'; 1280 | }) 1281 | .replace(splatParam, '(.*?)'); 1282 | return new RegExp('^' + route + '$'); 1283 | }, 1284 | 1285 | // Given a route, and a URL fragment that it matches, return the array of 1286 | // extracted decoded parameters. Empty or unmatched parameters will be 1287 | // treated as `null` to normalize cross-browser behavior. 1288 | _extractParameters: function(route, fragment) { 1289 | var params = route.exec(fragment).slice(1); 1290 | return _.map(params, function(param) { 1291 | return param ? decodeURIComponent(param) : null; 1292 | }); 1293 | } 1294 | 1295 | }); 1296 | 1297 | // Backbone.History 1298 | // ---------------- 1299 | 1300 | // Handles cross-browser history management, based on either 1301 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1302 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1303 | // and URL fragments. If the browser supports neither (old IE, natch), 1304 | // falls back to polling. 1305 | var History = Backbone.History = function() { 1306 | this.handlers = []; 1307 | _.bindAll(this, 'checkUrl'); 1308 | 1309 | // Ensure that `History` can be used outside of the browser. 1310 | if (typeof window !== 'undefined') { 1311 | this.location = window.location; 1312 | this.history = window.history; 1313 | } 1314 | }; 1315 | 1316 | // Cached regex for stripping a leading hash/slash and trailing space. 1317 | var routeStripper = /^[#\/]|\s+$/g; 1318 | 1319 | // Cached regex for stripping leading and trailing slashes. 1320 | var rootStripper = /^\/+|\/+$/g; 1321 | 1322 | // Cached regex for detecting MSIE. 1323 | var isExplorer = /msie [\w.]+/; 1324 | 1325 | // Cached regex for removing a trailing slash. 1326 | var trailingSlash = /\/$/; 1327 | 1328 | // Has the history handling already been started? 1329 | History.started = false; 1330 | 1331 | // Set up all inheritable **Backbone.History** properties and methods. 1332 | _.extend(History.prototype, Events, { 1333 | 1334 | // The default interval to poll for hash changes, if necessary, is 1335 | // twenty times a second. 1336 | interval: 50, 1337 | 1338 | // Gets the true hash value. Cannot use location.hash directly due to bug 1339 | // in Firefox where location.hash will always be decoded. 1340 | getHash: function(window) { 1341 | var match = (window || this).location.href.match(/#(.*)$/); 1342 | return match ? match[1] : ''; 1343 | }, 1344 | 1345 | // Get the cross-browser normalized URL fragment, either from the URL, 1346 | // the hash, or the override. 1347 | getFragment: function(fragment, forcePushState) { 1348 | if (fragment == null) { 1349 | if (this._hasPushState || !this._wantsHashChange || forcePushState) { 1350 | fragment = this.location.pathname; 1351 | var root = this.root.replace(trailingSlash, ''); 1352 | if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); 1353 | } else { 1354 | fragment = this.getHash(); 1355 | } 1356 | } 1357 | return fragment.replace(routeStripper, ''); 1358 | }, 1359 | 1360 | // Start the hash change handling, returning `true` if the current URL matches 1361 | // an existing route, and `false` otherwise. 1362 | start: function(options) { 1363 | if (History.started) throw new Error("Backbone.history has already been started"); 1364 | History.started = true; 1365 | 1366 | // Figure out the initial configuration. Do we need an iframe? 1367 | // Is pushState desired ... is it available? 1368 | this.options = _.extend({}, {root: '/'}, this.options, options); 1369 | this.root = this.options.root; 1370 | this._wantsHashChange = this.options.hashChange !== false; 1371 | this._wantsPushState = !!this.options.pushState; 1372 | this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); 1373 | var fragment = this.getFragment(); 1374 | var docMode = document.documentMode; 1375 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1376 | 1377 | // Normalize root to always include a leading and trailing slash. 1378 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1379 | 1380 | if (oldIE && this._wantsHashChange) { 1381 | this.iframe = Backbone.$('