├── .gitignore ├── Gruntfile.coffee ├── README.md ├── api ├── models │ └── todo.coffee └── routes │ └── todos.coffee ├── bashrc ├── package.json ├── server.coffee └── web ├── app.coffee ├── main.coffee ├── models └── todo.coffee ├── public ├── css │ ├── style.css │ └── style.styl ├── index.html └── js │ ├── app.js │ ├── lib │ ├── backbone.babysitter.js │ ├── backbone.js │ ├── backbone.marionette.js │ ├── backbone.relational.js │ ├── backbone.wreqr.js │ ├── jquery.js │ ├── require.js │ └── underscore.js │ ├── main.js │ ├── models │ └── todo.js │ ├── router.js │ ├── templates │ ├── jade.js │ ├── template.js │ └── todo.js │ └── views │ ├── template.js │ ├── todo.js │ └── todolist.js ├── router.coffee ├── templates ├── template.jade └── todo.jade └── views ├── .template.coffee.swp ├── template.coffee ├── todo.coffee └── todolist.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | *.swp 18 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | 3 | module.exports = (grunt) -> 4 | grunt.initConfig({ 5 | express: { 6 | server: { 7 | options: { 8 | bases: path.resolve('web/public') 9 | debug: true 10 | server: path.resolve('./server.coffee') 11 | monitor: { 12 | command: 'coffee' 13 | } 14 | } 15 | } 16 | } 17 | 18 | coffee: { 19 | compile: { 20 | files: [ 21 | expand: true 22 | cwd: 'web/' 23 | src: '**/*.coffee' 24 | dest: 'web/public/js/' 25 | ext: '.js' 26 | ] 27 | } 28 | } 29 | 30 | jade: { 31 | compile: { 32 | options: { 33 | client: true 34 | namespace: false 35 | amd: true 36 | } 37 | files: [ 38 | expand: true 39 | cwd: 'web/templates' 40 | src: '**/*.jade' 41 | dest: 'web/public/js/templates' 42 | ext: '.js' 43 | ] 44 | } 45 | } 46 | 47 | watch: { 48 | coffee: { 49 | files: 'web/**/*.coffee' 50 | tasks: ['coffee'] 51 | } 52 | jade: { 53 | files: 'web/templates/*.jade' 54 | tasks: ['jade'] 55 | } 56 | } 57 | 58 | }) 59 | 60 | grunt.loadNpmTasks('grunt-contrib-coffee') 61 | grunt.loadNpmTasks('grunt-contrib-jade') 62 | grunt.loadNpmTasks('grunt-contrib-requirejs') 63 | grunt.loadNpmTasks('grunt-contrib-watch') 64 | grunt.loadNpmTasks('grunt-express') 65 | 66 | grunt.registerTask('build', 'coffee jade stylus requirejs') 67 | grunt.registerTask('build-dev', 'coffee jade') 68 | grunt.registerTask('default', ['express', 'watch']) 69 | 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node / Backbone Web App Boilerplate 2 | 3 | ## Description 4 | Node/Backbone forms a powerful combination for building single page web applications that scale. Because of these two project's popularity, there are several great boilerplate projects out there, but all for either Node or Backbone seperately. This project aims to integrate these two worlds (Backend and Frontend) making it easy to start building next generation web applications. 5 | 6 | The most prominent technologies are: 7 | 8 | #### Backend: 9 | - Node.js 10 | - Express.js 11 | - Mongoose.js / MongoDB 12 | 13 | #### Frontend: 14 | - Backbone.js 15 | - Marionette.js 16 | - Require.js 17 | - Jade Templates 18 | - Stylus CSS Preprocessor 19 | 20 | ### Other 21 | - CoffeeScript 22 | - Grunt based development environment 23 | 24 | Although it should already provide a solid starting point, this project is still a work in progress (See Todo's below). 25 | 26 | ## Getting started 27 | 28 | 1. In an Ubuntu 12.10 terminal, run the following command to install the required dependencies. (Or use your distribution's instructions to install Git, Node v0.8, NPM and MongoDB.) 29 | 30 | sudo apt-get install git nodejs npm mongodb-server 31 | 32 | 2. Get the project source code from GitHub 33 | 34 | git clone git@github.com:skaapgif/webapp-boilerplate.git webapp 35 | cd webapp 36 | 37 | 3. Optionally run these commands to set up the node package manager to install node packages in your home folder (useful if you don't have sudo rights, like at a university lab) 38 | 39 | cat npmrc >> ~/.npmrc 40 | cat bashrc >> ~/.bashrc 41 | source ~/.bashrc 42 | 43 | 4. Finally, install node package dependencies and start the server 44 | 45 | npm install 46 | npm install -g grunt-cli 47 | npm start 48 | 49 | 5. Point your browser to localhost:3000 and start hacking! Any changes to source files will cause grunt to recompile/reload. 50 | 51 | ## Todo 52 | - Mocha BDD api tests 53 | - Mocha BDD frontend tests using WebDriver 54 | - Introduction to folder layout & files, links to relevant tutorials 55 | - Example stylus css 56 | - MongoDB sessions 57 | - Production hardening (Cluster, error handling, monitoring) 58 | - Heroku Procfile and instructions for super easy heroku deployments 59 | -------------------------------------------------------------------------------- /api/models/todo.coffee: -------------------------------------------------------------------------------- 1 | mongoose = require('mongoose') 2 | Schema = mongoose.Schema 3 | 4 | TodoSchema = new Schema({ 5 | title: String 6 | done: Boolean 7 | }) 8 | 9 | return mongoose.model('Todo', TodoSchema) 10 | -------------------------------------------------------------------------------- /api/routes/todos.coffee: -------------------------------------------------------------------------------- 1 | 2 | Todo = require('../models/todo') 3 | 4 | # List todos 5 | exports.list = (req, res) -> 6 | res.json([{title: 'First todo'}]) 7 | 8 | exports.post = (req, res) -> 9 | todo = new Todo({title: 'hello'}) 10 | todo.save((err) -> 11 | if err? then next(err) 12 | res.json(todo) 13 | ) 14 | 15 | exports.put = (req, res) -> 16 | res.send(404, 'Implement') 17 | -------------------------------------------------------------------------------- /bashrc: -------------------------------------------------------------------------------- 1 | ### Add globally install node package binaries to path 2 | export PATH="$HOME/.npm/bin:$PATH" 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "grunt" 7 | }, 8 | "dependencies": { 9 | "express": "3.0.x", 10 | "jade": "0.28.x", 11 | "stylus": "0.32.x", 12 | "grunt": "0.4.x", 13 | "grunt-contrib-jade": "0.5.x", 14 | "grunt-contrib-coffee": "0.6.x", 15 | "grunt-contrib-watch": "~0.3.1", 16 | "grunt-express": "~0.3.0", 17 | "mongoose": "~3.5.9", 18 | "grunt-contrib-requirejs": "~0.4.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server.coffee: -------------------------------------------------------------------------------- 1 | http = require("http") 2 | express = require("express") 3 | todos = require("./api/routes/todos") 4 | mongoose = require('mongoose') 5 | #http = require("http") 6 | path = require("path") 7 | 8 | mongoose.connect('localhost', 'test') 9 | 10 | app = express() 11 | app.configure(() -> 12 | app.set("port", process.env.PORT or 3000) 13 | app.use(express.favicon()) 14 | app.use(express.logger("dev")) 15 | app.use(express.bodyParser()) 16 | app.use(app.router) 17 | app.use(express.static(path.join(__dirname, "/web/public"))) 18 | ) 19 | 20 | app.configure("development", () -> 21 | app.use(express.errorHandler()) 22 | ) 23 | 24 | #app.get("/", routes.index) 25 | app.get("/todos", todos.list) 26 | app.put("/todos", todos.post) 27 | app.put("/todos/:id", todos.put) 28 | 29 | module.exports = app 30 | 31 | ### Handled by grunt-express 32 | http.createServer(app).listen(app.get("port"), () -> 33 | console.log "Express server listening on port " + app.get("port") 34 | ) 35 | ### 36 | -------------------------------------------------------------------------------- /web/app.coffee: -------------------------------------------------------------------------------- 1 | define((require) -> 2 | $ = require('jquery') 3 | Backbone = require('backbone') 4 | Marionette = require('marionette') 5 | router = require('router') 6 | 7 | app = new Marionette.Application() 8 | app.router = router 9 | 10 | app.addInitializer((options) -> 11 | Backbone.history.start() 12 | ) 13 | 14 | app.addRegions({ 15 | content: '#content' 16 | }) 17 | 18 | return app 19 | ) 20 | -------------------------------------------------------------------------------- /web/main.coffee: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | baseUrl: 'js' 3 | paths: { 4 | 'backbone': 'lib/backbone' 5 | 'backbone.babysitter': 'lib/backbone.babysitter' 6 | 'backbone.wreqr': 'lib/backbone.wreqr' 7 | 'jade': 'templates/jade' 8 | 'jquery': 'lib/jquery' 9 | 'marionette': 'lib/backbone.marionette' 10 | 'underscore': 'lib/underscore' 11 | } 12 | shim: { 13 | } 14 | }) 15 | 16 | require(['app'], (app) -> 17 | 18 | app.start() 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /web/models/todo.coffee: -------------------------------------------------------------------------------- 1 | define((require) -> 2 | 3 | Backbone = require('backbone') 4 | 5 | return Backbone.Model.extend({ 6 | urlRoot: '/todos/' 7 | }) 8 | ) 9 | -------------------------------------------------------------------------------- /web/public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | a { 6 | color: #00b7ff; 7 | } 8 | -------------------------------------------------------------------------------- /web/public/css/style.styl: -------------------------------------------------------------------------------- 1 | body 2 | padding: 50px 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif 4 | a 5 | color: #00B7FF -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Todo List 7 | 8 | 9 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /web/public/js/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | define(function(require) { 4 | var $, Backbone, Marionette, app, router; 5 | $ = require('jquery'); 6 | Backbone = require('backbone'); 7 | Marionette = require('marionette'); 8 | router = require('router'); 9 | app = new Marionette.Application(); 10 | app.router = router; 11 | app.addInitializer(function(options) { 12 | return Backbone.history.start(); 13 | }); 14 | app.addRegions({ 15 | content: '#content' 16 | }); 17 | return app; 18 | }); 19 | 20 | }).call(this); 21 | -------------------------------------------------------------------------------- /web/public/js/lib/backbone.babysitter.js: -------------------------------------------------------------------------------- 1 | // Backbone.BabySitter, v0.0.4 2 | // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC. 3 | // Distributed under MIT license 4 | // http://github.com/marionettejs/backbone.babysitter 5 | (function (root, factory) { 6 | if (typeof exports === 'object') { 7 | 8 | var underscore = require('underscore'); 9 | var backbone = require('backbone'); 10 | 11 | module.exports = factory(underscore, backbone); 12 | 13 | } else if (typeof define === 'function' && define.amd) { 14 | 15 | define(['underscore', 'backbone'], factory); 16 | 17 | } 18 | }(this, function (_, Backbone) { 19 | "option strict"; 20 | 21 | // Backbone.ChildViewContainer 22 | // --------------------------- 23 | // 24 | // Provide a container to store, retrieve and 25 | // shut down child views. 26 | 27 | Backbone.ChildViewContainer = (function(Backbone, _){ 28 | 29 | // Container Constructor 30 | // --------------------- 31 | 32 | var Container = function(initialViews){ 33 | this._views = {}; 34 | this._indexByModel = {}; 35 | this._indexByCollection = {}; 36 | this._indexByCustom = {}; 37 | this._updateLength(); 38 | 39 | this._addInitialViews(initialViews); 40 | }; 41 | 42 | // Container Methods 43 | // ----------------- 44 | 45 | _.extend(Container.prototype, { 46 | 47 | // Add a view to this container. Stores the view 48 | // by `cid` and makes it searchable by the model 49 | // and/or collection of the view. Optionally specify 50 | // a custom key to store an retrieve the view. 51 | add: function(view, customIndex){ 52 | var viewCid = view.cid; 53 | 54 | // store the view 55 | this._views[viewCid] = view; 56 | 57 | // index it by model 58 | if (view.model){ 59 | this._indexByModel[view.model.cid] = viewCid; 60 | } 61 | 62 | // index it by collection 63 | if (view.collection){ 64 | this._indexByCollection[view.collection.cid] = viewCid; 65 | } 66 | 67 | // index by custom 68 | if (customIndex){ 69 | this._indexByCustom[customIndex] = viewCid; 70 | } 71 | 72 | this._updateLength(); 73 | }, 74 | 75 | // Find a view by the model that was attached to 76 | // it. Uses the model's `cid` to find it, and 77 | // retrieves the view by it's `cid` from the result 78 | findByModel: function(model){ 79 | var viewCid = this._indexByModel[model.cid]; 80 | return this.findByCid(viewCid); 81 | }, 82 | 83 | // Find a view by the collection that was attached to 84 | // it. Uses the collection's `cid` to find it, and 85 | // retrieves the view by it's `cid` from the result 86 | findByCollection: function(col){ 87 | var viewCid = this._indexByCollection[col.cid]; 88 | return this.findByCid(viewCid); 89 | }, 90 | 91 | // Find a view by a custom indexer. 92 | findByCustom: function(index){ 93 | var viewCid = this._indexByCustom[index]; 94 | return this.findByCid(viewCid); 95 | }, 96 | 97 | // Find by index. This is not guaranteed to be a 98 | // stable index. 99 | findByIndex: function(index){ 100 | return _.values(this._views)[index]; 101 | }, 102 | 103 | // retrieve a view by it's `cid` directly 104 | findByCid: function(cid){ 105 | return this._views[cid]; 106 | }, 107 | 108 | // Remove a view 109 | remove: function(view){ 110 | var viewCid = view.cid; 111 | 112 | // delete model index 113 | if (view.model){ 114 | delete this._indexByModel[view.model.cid]; 115 | } 116 | 117 | // delete collection index 118 | if (view.collection){ 119 | delete this._indexByCollection[view.collection.cid]; 120 | } 121 | 122 | // delete custom index 123 | var cust; 124 | 125 | for (var key in this._indexByCustom){ 126 | if (this._indexByCustom.hasOwnProperty(key)){ 127 | if (this._indexByCustom[key] === viewCid){ 128 | cust = key; 129 | break; 130 | } 131 | } 132 | } 133 | 134 | if (cust){ 135 | delete this._indexByCustom[cust]; 136 | } 137 | 138 | // remove the view from the container 139 | delete this._views[viewCid]; 140 | 141 | // update the length 142 | this._updateLength(); 143 | }, 144 | 145 | // Call a method on every view in the container, 146 | // passing parameters to the call method one at a 147 | // time, like `function.call`. 148 | call: function(method, args){ 149 | args = Array.prototype.slice.call(arguments, 1); 150 | this.apply(method, args); 151 | }, 152 | 153 | // Apply a method on every view in the container, 154 | // passing parameters to the call method one at a 155 | // time, like `function.apply`. 156 | apply: function(method, args){ 157 | var view; 158 | 159 | // fix for IE < 9 160 | args = args || []; 161 | 162 | _.each(this._views, function(view, key){ 163 | if (_.isFunction(view[method])){ 164 | view[method].apply(view, args); 165 | } 166 | }); 167 | 168 | }, 169 | 170 | // Update the `.length` attribute on this container 171 | _updateLength: function(){ 172 | this.length = _.size(this._views); 173 | }, 174 | 175 | // set up an initial list of views 176 | _addInitialViews: function(views){ 177 | if (!views){ return; } 178 | 179 | var view, i, 180 | length = views.length; 181 | 182 | for (i=0; i').hide().appendTo('body')[0].contentWindow; 1093 | this.navigate(fragment); 1094 | } 1095 | 1096 | // Depending on whether we're using pushState or hashes, and whether 1097 | // 'onhashchange' is supported, determine how we check the URL state. 1098 | if (this._hasPushState) { 1099 | Backbone.$(window).on('popstate', this.checkUrl); 1100 | } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { 1101 | Backbone.$(window).on('hashchange', this.checkUrl); 1102 | } else if (this._wantsHashChange) { 1103 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval); 1104 | } 1105 | 1106 | // Determine if we need to change the base url, for a pushState link 1107 | // opened by a non-pushState browser. 1108 | this.fragment = fragment; 1109 | var loc = this.location; 1110 | var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; 1111 | 1112 | // If we've started off with a route from a `pushState`-enabled browser, 1113 | // but we're currently in a browser that doesn't support it... 1114 | if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) { 1115 | this.fragment = this.getFragment(null, true); 1116 | this.location.replace(this.root + this.location.search + '#' + this.fragment); 1117 | // Return immediately as browser will do redirect to new url 1118 | return true; 1119 | 1120 | // Or if we've started out with a hash-based route, but we're currently 1121 | // in a browser where it could be `pushState`-based instead... 1122 | } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { 1123 | this.fragment = this.getHash().replace(routeStripper, ''); 1124 | this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); 1125 | } 1126 | 1127 | if (!this.options.silent) return this.loadUrl(); 1128 | }, 1129 | 1130 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app, 1131 | // but possibly useful for unit testing Routers. 1132 | stop: function() { 1133 | Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); 1134 | clearInterval(this._checkUrlInterval); 1135 | History.started = false; 1136 | }, 1137 | 1138 | // Add a route to be tested when the fragment changes. Routes added later 1139 | // may override previous routes. 1140 | route: function(route, callback) { 1141 | this.handlers.unshift({route: route, callback: callback}); 1142 | }, 1143 | 1144 | // Checks the current URL to see if it has changed, and if it has, 1145 | // calls `loadUrl`, normalizing across the hidden iframe. 1146 | checkUrl: function(e) { 1147 | var current = this.getFragment(); 1148 | if (current === this.fragment && this.iframe) { 1149 | current = this.getFragment(this.getHash(this.iframe)); 1150 | } 1151 | if (current === this.fragment) return false; 1152 | if (this.iframe) this.navigate(current); 1153 | this.loadUrl() || this.loadUrl(this.getHash()); 1154 | }, 1155 | 1156 | // Attempt to load the current URL fragment. If a route succeeds with a 1157 | // match, returns `true`. If no defined routes matches the fragment, 1158 | // returns `false`. 1159 | loadUrl: function(fragmentOverride) { 1160 | var fragment = this.fragment = this.getFragment(fragmentOverride); 1161 | var matched = _.any(this.handlers, function(handler) { 1162 | if (handler.route.test(fragment)) { 1163 | handler.callback(fragment); 1164 | return true; 1165 | } 1166 | }); 1167 | return matched; 1168 | }, 1169 | 1170 | // Save a fragment into the hash history, or replace the URL state if the 1171 | // 'replace' option is passed. You are responsible for properly URL-encoding 1172 | // the fragment in advance. 1173 | // 1174 | // The options object can contain `trigger: true` if you wish to have the 1175 | // route callback be fired (not usually desirable), or `replace: true`, if 1176 | // you wish to modify the current URL without adding an entry to the history. 1177 | navigate: function(fragment, options) { 1178 | if (!History.started) return false; 1179 | if (!options || options === true) options = {trigger: options}; 1180 | fragment = this.getFragment(fragment || ''); 1181 | if (this.fragment === fragment) return; 1182 | this.fragment = fragment; 1183 | var url = this.root + fragment; 1184 | 1185 | // If pushState is available, we use it to set the fragment as a real URL. 1186 | if (this._hasPushState) { 1187 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); 1188 | 1189 | // If hash changes haven't been explicitly disabled, update the hash 1190 | // fragment to store history. 1191 | } else if (this._wantsHashChange) { 1192 | this._updateHash(this.location, fragment, options.replace); 1193 | if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { 1194 | // Opening and closing the iframe tricks IE7 and earlier to push a 1195 | // history entry on hash-tag change. When replace is true, we don't 1196 | // want this. 1197 | if(!options.replace) this.iframe.document.open().close(); 1198 | this._updateHash(this.iframe.location, fragment, options.replace); 1199 | } 1200 | 1201 | // If you've told us that you explicitly don't want fallback hashchange- 1202 | // based history, then `navigate` becomes a page refresh. 1203 | } else { 1204 | return this.location.assign(url); 1205 | } 1206 | if (options.trigger) this.loadUrl(fragment); 1207 | }, 1208 | 1209 | // Update the hash location, either replacing the current entry, or adding 1210 | // a new one to the browser history. 1211 | _updateHash: function(location, fragment, replace) { 1212 | if (replace) { 1213 | var href = location.href.replace(/(javascript:|#).*$/, ''); 1214 | location.replace(href + '#' + fragment); 1215 | } else { 1216 | // gh-1649: Some browsers require that `hash` contains a leading #. 1217 | location.hash = '#' + fragment; 1218 | } 1219 | } 1220 | 1221 | }); 1222 | 1223 | // Create the default Backbone.history. 1224 | Backbone.history = new History; 1225 | 1226 | // Backbone.View 1227 | // ------------- 1228 | 1229 | // Creating a Backbone.View creates its initial element outside of the DOM, 1230 | // if an existing element is not provided... 1231 | var View = Backbone.View = function(options) { 1232 | this.cid = _.uniqueId('view'); 1233 | this._configure(options || {}); 1234 | this._ensureElement(); 1235 | this.initialize.apply(this, arguments); 1236 | this.delegateEvents(); 1237 | }; 1238 | 1239 | // Cached regex to split keys for `delegate`. 1240 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 1241 | 1242 | // List of view options to be merged as properties. 1243 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 1244 | 1245 | // Set up all inheritable **Backbone.View** properties and methods. 1246 | _.extend(View.prototype, Events, { 1247 | 1248 | // The default `tagName` of a View's element is `"div"`. 1249 | tagName: 'div', 1250 | 1251 | // jQuery delegate for element lookup, scoped to DOM elements within the 1252 | // current view. This should be prefered to global lookups where possible. 1253 | $: function(selector) { 1254 | return this.$el.find(selector); 1255 | }, 1256 | 1257 | // Initialize is an empty function by default. Override it with your own 1258 | // initialization logic. 1259 | initialize: function(){}, 1260 | 1261 | // **render** is the core function that your view should override, in order 1262 | // to populate its element (`this.el`), with the appropriate HTML. The 1263 | // convention is for **render** to always return `this`. 1264 | render: function() { 1265 | return this; 1266 | }, 1267 | 1268 | // Remove this view by taking the element out of the DOM, and removing any 1269 | // applicable Backbone.Events listeners. 1270 | remove: function() { 1271 | this.$el.remove(); 1272 | this.stopListening(); 1273 | return this; 1274 | }, 1275 | 1276 | // Change the view's element (`this.el` property), including event 1277 | // re-delegation. 1278 | setElement: function(element, delegate) { 1279 | if (this.$el) this.undelegateEvents(); 1280 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); 1281 | this.el = this.$el[0]; 1282 | if (delegate !== false) this.delegateEvents(); 1283 | return this; 1284 | }, 1285 | 1286 | // Set callbacks, where `this.events` is a hash of 1287 | // 1288 | // *{"event selector": "callback"}* 1289 | // 1290 | // { 1291 | // 'mousedown .title': 'edit', 1292 | // 'click .button': 'save' 1293 | // 'click .open': function(e) { ... } 1294 | // } 1295 | // 1296 | // pairs. Callbacks will be bound to the view, with `this` set properly. 1297 | // Uses event delegation for efficiency. 1298 | // Omitting the selector binds the event to `this.el`. 1299 | // This only works for delegate-able events: not `focus`, `blur`, and 1300 | // not `change`, `submit`, and `reset` in Internet Explorer. 1301 | delegateEvents: function(events) { 1302 | if (!(events || (events = _.result(this, 'events')))) return; 1303 | this.undelegateEvents(); 1304 | for (var key in events) { 1305 | var method = events[key]; 1306 | if (!_.isFunction(method)) method = this[events[key]]; 1307 | if (!method) throw new Error('Method "' + events[key] + '" does not exist'); 1308 | var match = key.match(delegateEventSplitter); 1309 | var eventName = match[1], selector = match[2]; 1310 | method = _.bind(method, this); 1311 | eventName += '.delegateEvents' + this.cid; 1312 | if (selector === '') { 1313 | this.$el.on(eventName, method); 1314 | } else { 1315 | this.$el.on(eventName, selector, method); 1316 | } 1317 | } 1318 | }, 1319 | 1320 | // Clears all callbacks previously bound to the view with `delegateEvents`. 1321 | // You usually don't need to use this, but may wish to if you have multiple 1322 | // Backbone views attached to the same DOM element. 1323 | undelegateEvents: function() { 1324 | this.$el.off('.delegateEvents' + this.cid); 1325 | }, 1326 | 1327 | // Performs the initial configuration of a View with a set of options. 1328 | // Keys with special meaning *(model, collection, id, className)*, are 1329 | // attached directly to the view. 1330 | _configure: function(options) { 1331 | if (this.options) options = _.extend({}, _.result(this, 'options'), options); 1332 | _.extend(this, _.pick(options, viewOptions)); 1333 | this.options = options; 1334 | }, 1335 | 1336 | // Ensure that the View has a DOM element to render into. 1337 | // If `this.el` is a string, pass it through `$()`, take the first 1338 | // matching element, and re-assign it to `el`. Otherwise, create 1339 | // an element from the `id`, `className` and `tagName` properties. 1340 | _ensureElement: function() { 1341 | if (!this.el) { 1342 | var attrs = _.extend({}, _.result(this, 'attributes')); 1343 | if (this.id) attrs.id = _.result(this, 'id'); 1344 | if (this.className) attrs['class'] = _.result(this, 'className'); 1345 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); 1346 | this.setElement($el, false); 1347 | } else { 1348 | this.setElement(_.result(this, 'el'), false); 1349 | } 1350 | } 1351 | 1352 | }); 1353 | 1354 | // Backbone.sync 1355 | // ------------- 1356 | 1357 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1358 | var methodMap = { 1359 | 'create': 'POST', 1360 | 'update': 'PUT', 1361 | 'patch': 'PATCH', 1362 | 'delete': 'DELETE', 1363 | 'read': 'GET' 1364 | }; 1365 | 1366 | // Override this function to change the manner in which Backbone persists 1367 | // models to the server. You will be passed the type of request, and the 1368 | // model in question. By default, makes a RESTful Ajax request 1369 | // to the model's `url()`. Some possible customizations could be: 1370 | // 1371 | // * Use `setTimeout` to batch rapid-fire updates into a single request. 1372 | // * Send up the models as XML instead of JSON. 1373 | // * Persist models via WebSockets instead of Ajax. 1374 | // 1375 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1376 | // as `POST`, with a `_method` parameter containing the true HTTP method, 1377 | // as well as all requests with the body as `application/x-www-form-urlencoded` 1378 | // instead of `application/json` with the model in a param named `model`. 1379 | // Useful when interfacing with server-side languages like **PHP** that make 1380 | // it difficult to read the body of `PUT` requests. 1381 | Backbone.sync = function(method, model, options) { 1382 | var type = methodMap[method]; 1383 | 1384 | // Default options, unless specified. 1385 | _.defaults(options || (options = {}), { 1386 | emulateHTTP: Backbone.emulateHTTP, 1387 | emulateJSON: Backbone.emulateJSON 1388 | }); 1389 | 1390 | // Default JSON-request options. 1391 | var params = {type: type, dataType: 'json'}; 1392 | 1393 | // Ensure that we have a URL. 1394 | if (!options.url) { 1395 | params.url = _.result(model, 'url') || urlError(); 1396 | } 1397 | 1398 | // Ensure that we have the appropriate request data. 1399 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1400 | params.contentType = 'application/json'; 1401 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1402 | } 1403 | 1404 | // For older servers, emulate JSON by encoding the request into an HTML-form. 1405 | if (options.emulateJSON) { 1406 | params.contentType = 'application/x-www-form-urlencoded'; 1407 | params.data = params.data ? {model: params.data} : {}; 1408 | } 1409 | 1410 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1411 | // And an `X-HTTP-Method-Override` header. 1412 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1413 | params.type = 'POST'; 1414 | if (options.emulateJSON) params.data._method = type; 1415 | var beforeSend = options.beforeSend; 1416 | options.beforeSend = function(xhr) { 1417 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 1418 | if (beforeSend) return beforeSend.apply(this, arguments); 1419 | }; 1420 | } 1421 | 1422 | // Don't process data on a non-GET request. 1423 | if (params.type !== 'GET' && !options.emulateJSON) { 1424 | params.processData = false; 1425 | } 1426 | 1427 | var success = options.success; 1428 | options.success = function(resp) { 1429 | if (success) success(model, resp, options); 1430 | model.trigger('sync', model, resp, options); 1431 | }; 1432 | 1433 | var error = options.error; 1434 | options.error = function(xhr) { 1435 | if (error) error(model, xhr, options); 1436 | model.trigger('error', model, xhr, options); 1437 | }; 1438 | 1439 | // Make the request, allowing the user to override any Ajax options. 1440 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1441 | model.trigger('request', model, xhr, options); 1442 | return xhr; 1443 | }; 1444 | 1445 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1446 | Backbone.ajax = function() { 1447 | return Backbone.$.ajax.apply(Backbone.$, arguments); 1448 | }; 1449 | 1450 | // Helpers 1451 | // ------- 1452 | 1453 | // Helper function to correctly set up the prototype chain, for subclasses. 1454 | // Similar to `goog.inherits`, but uses a hash of prototype properties and 1455 | // class properties to be extended. 1456 | var extend = function(protoProps, staticProps) { 1457 | var parent = this; 1458 | var child; 1459 | 1460 | // The constructor function for the new subclass is either defined by you 1461 | // (the "constructor" property in your `extend` definition), or defaulted 1462 | // by us to simply call the parent's constructor. 1463 | if (protoProps && _.has(protoProps, 'constructor')) { 1464 | child = protoProps.constructor; 1465 | } else { 1466 | child = function(){ return parent.apply(this, arguments); }; 1467 | } 1468 | 1469 | // Add static properties to the constructor function, if supplied. 1470 | _.extend(child, parent, staticProps); 1471 | 1472 | // Set the prototype chain to inherit from `parent`, without calling 1473 | // `parent`'s constructor function. 1474 | var Surrogate = function(){ this.constructor = child; }; 1475 | Surrogate.prototype = parent.prototype; 1476 | child.prototype = new Surrogate; 1477 | 1478 | // Add prototype properties (instance properties) to the subclass, 1479 | // if supplied. 1480 | if (protoProps) _.extend(child.prototype, protoProps); 1481 | 1482 | // Set a convenience property in case the parent's prototype is needed 1483 | // later. 1484 | child.__super__ = parent.prototype; 1485 | 1486 | return child; 1487 | }; 1488 | 1489 | // Set up inheritance for the model, collection, router, view and history. 1490 | Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; 1491 | 1492 | // Throw an error when a URL is needed, and none is supplied. 1493 | var urlError = function() { 1494 | throw new Error('A "url" property or function must be specified'); 1495 | }; 1496 | 1497 | return Backbone; 1498 | })); 1499 | -------------------------------------------------------------------------------- /web/public/js/lib/backbone.marionette.js: -------------------------------------------------------------------------------- 1 | // Backbone.Marionette, v1.0.0-rc5 2 | // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. 3 | // Distributed under MIT license 4 | // http://github.com/marionettejs/backbone.marionette 5 | 6 | (function (root, factory) { 7 | if (typeof exports === 'object') { 8 | 9 | var jquery = require('jquery'); 10 | var underscore = require('underscore'); 11 | var backbone = require('backbone'); 12 | var wreqr = require('backbone.wreqr'); 13 | var babysitter = require('backbone.babysitter'); 14 | 15 | module.exports = factory(jquery, underscore, backbone, wreqr, babysitter); 16 | 17 | } else if (typeof define === 'function' && define.amd) { 18 | 19 | define(['jquery', 'underscore', 'backbone', 'backbone.wreqr', 'backbone.babysitter'], factory); 20 | 21 | } 22 | }(this, function ($, _, Backbone) { 23 | 24 | var Marionette = (function(Backbone, _, $){ 25 | "use strict"; 26 | 27 | var Marionette = {}; 28 | Backbone.Marionette = Marionette; 29 | 30 | // Helpers 31 | // ------- 32 | 33 | // For slicing `arguments` in functions 34 | var slice = Array.prototype.slice; 35 | 36 | // Marionette.extend 37 | // ----------------- 38 | 39 | // Borrow the Backbone `extend` method so we can use it as needed 40 | Marionette.extend = Backbone.Model.extend; 41 | 42 | // Marionette.getOption 43 | // -------------------- 44 | 45 | // Retrieve an object, function or other value from a target 46 | // object or it's `options`, with `options` taking precedence. 47 | Marionette.getOption = function(target, optionName){ 48 | if (!target || !optionName){ return; } 49 | var value; 50 | 51 | if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)){ 52 | value = target.options[optionName]; 53 | } else { 54 | value = target[optionName]; 55 | } 56 | 57 | return value; 58 | }; 59 | 60 | // Mairionette.createObject 61 | // ------------------------ 62 | 63 | // A wrapper / shim for `Object.create`. Uses native `Object.create` 64 | // if available, otherwise shims it in place for Marionette to use. 65 | Marionette.createObject = (function(){ 66 | var createObject; 67 | 68 | // Define this once, and just replace the .prototype on it as needed, 69 | // to improve performance in older / less optimized JS engines 70 | function F() {} 71 | 72 | 73 | // Check for existing native / shimmed Object.create 74 | if (typeof Object.create === "function"){ 75 | 76 | // found native/shim, so use it 77 | createObject = Object.create; 78 | 79 | } else { 80 | 81 | // An implementation of the Boodman/Crockford delegation 82 | // w/ Cornford optimization, as suggested by @unscriptable 83 | // https://gist.github.com/3959151 84 | 85 | // native/shim not found, so shim it ourself 86 | createObject = function (o) { 87 | 88 | // set the prototype of the function 89 | // so we will get `o` as the prototype 90 | // of the new object instance 91 | F.prototype = o; 92 | 93 | // create a new object that inherits from 94 | // the `o` parameter 95 | var child = new F(); 96 | 97 | // clean up just in case o is really large 98 | F.prototype = null; 99 | 100 | // send it back 101 | return child; 102 | }; 103 | 104 | } 105 | 106 | return createObject; 107 | })(); 108 | 109 | // Trigger an event and a corresponding method name. Examples: 110 | // 111 | // `this.triggerMethod("foo")` will trigger the "foo" event and 112 | // call the "onFoo" method. 113 | // 114 | // `this.triggerMethod("foo:bar") will trigger the "foo:bar" event and 115 | // call the "onFooBar" method. 116 | Marionette.triggerMethod = function(){ 117 | var args = Array.prototype.slice.apply(arguments); 118 | var eventName = args[0]; 119 | var segments = eventName.split(":"); 120 | var segment, capLetter, methodName = "on"; 121 | 122 | for (var i = 0; i < segments.length; i++){ 123 | segment = segments[i]; 124 | capLetter = segment.charAt(0).toUpperCase(); 125 | methodName += capLetter + segment.slice(1); 126 | } 127 | 128 | this.trigger.apply(this, args); 129 | 130 | if (_.isFunction(this[methodName])){ 131 | args.shift(); 132 | return this[methodName].apply(this, args); 133 | } 134 | }; 135 | 136 | // DOMRefresh 137 | // ---------- 138 | // 139 | // Monitor a view's state, and after it has been rendered and shown 140 | // in the DOM, trigger a "dom:refresh" event every time it is 141 | // re-rendered. 142 | 143 | Marionette.MonitorDOMRefresh = (function(){ 144 | // track when the view has been rendered 145 | function handleShow(view){ 146 | view._isShown = true; 147 | triggerDOMRefresh(view); 148 | } 149 | 150 | // track when the view has been shown in the DOM, 151 | // using a Marionette.Region (or by other means of triggering "show") 152 | function handleRender(view){ 153 | view._isRendered = true; 154 | triggerDOMRefresh(view); 155 | } 156 | 157 | // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method 158 | function triggerDOMRefresh(view){ 159 | if (view._isShown && view._isRendered){ 160 | if (_.isFunction(view.triggerMethod)){ 161 | view.triggerMethod("dom:refresh"); 162 | } 163 | } 164 | } 165 | 166 | // Export public API 167 | return function(view){ 168 | view.listenTo(view, "show", function(){ 169 | handleShow(view); 170 | }); 171 | 172 | view.listenTo(view, "render", function(){ 173 | handleRender(view); 174 | }); 175 | }; 176 | })(); 177 | 178 | 179 | // Marionette.bindEntityEvents & unbindEntityEvents 180 | // --------------------------- 181 | // 182 | // These methods are used to bind/unbind a backbone "entity" (collection/model) 183 | // to methods on a target object. 184 | // 185 | // The first paremter, `target`, must have a `listenTo` method from the 186 | // EventBinder object. 187 | // 188 | // The second parameter is the entity (Backbone.Model or Backbone.Collection) 189 | // to bind the events from. 190 | // 191 | // The third parameter is a hash of { "event:name": "eventHandler" } 192 | // configuration. Multiple handlers can be separated by a space. A 193 | // function can be supplied instead of a string handler name. 194 | 195 | (function(Marionette){ 196 | "use strict"; 197 | 198 | // Bind the event to handlers specified as a string of 199 | // handler names on the target object 200 | function bindFromStrings(target, entity, evt, methods){ 201 | var methodNames = methods.split(/\s+/); 202 | 203 | _.each(methodNames,function(methodName) { 204 | 205 | var method = target[methodName]; 206 | if(!method) { 207 | throw new Error("Method '"+ methodName +"' was configured as an event handler, but does not exist."); 208 | } 209 | 210 | target.listenTo(entity, evt, method, target); 211 | }); 212 | } 213 | 214 | // Bind the event to a supplied callback function 215 | function bindToFunction(target, entity, evt, method){ 216 | target.listenTo(entity, evt, method, target); 217 | } 218 | 219 | // Bind the event to handlers specified as a string of 220 | // handler names on the target object 221 | function unbindFromStrings(target, entity, evt, methods){ 222 | var methodNames = methods.split(/\s+/); 223 | 224 | _.each(methodNames,function(methodName) { 225 | var method = target[method]; 226 | target.stopListening(entity, evt, method, target); 227 | }); 228 | } 229 | 230 | // Bind the event to a supplied callback function 231 | function unbindToFunction(target, entity, evt, method){ 232 | target.stopListening(entity, evt, method, target); 233 | } 234 | 235 | 236 | // generic looping function 237 | function iterateEvents(target, entity, bindings, functionCallback, stringCallback){ 238 | if (!entity || !bindings) { return; } 239 | 240 | // allow the bindings to be a function 241 | if (_.isFunction(bindings)){ 242 | bindings = bindings.call(target); 243 | } 244 | 245 | // iterate the bindings and bind them 246 | _.each(bindings, function(methods, evt){ 247 | 248 | // allow for a function as the handler, 249 | // or a list of event names as a string 250 | if (_.isFunction(methods)){ 251 | functionCallback(target, entity, evt, methods); 252 | } else { 253 | stringCallback(target, entity, evt, methods); 254 | } 255 | 256 | }); 257 | } 258 | 259 | // Export Public API 260 | Marionette.bindEntityEvents = function(target, entity, bindings){ 261 | iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); 262 | }; 263 | 264 | Marionette.unbindEntityEvents = function(target, entity, bindings){ 265 | iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); 266 | }; 267 | 268 | })(Marionette); 269 | 270 | 271 | // Callbacks 272 | // --------- 273 | 274 | // A simple way of managing a collection of callbacks 275 | // and executing them at a later point in time, using jQuery's 276 | // `Deferred` object. 277 | Marionette.Callbacks = function(){ 278 | this._deferred = $.Deferred(); 279 | this._callbacks = []; 280 | }; 281 | 282 | _.extend(Marionette.Callbacks.prototype, { 283 | 284 | // Add a callback to be executed. Callbacks added here are 285 | // guaranteed to execute, even if they are added after the 286 | // `run` method is called. 287 | add: function(callback, contextOverride){ 288 | this._callbacks.push({cb: callback, ctx: contextOverride}); 289 | 290 | this._deferred.done(function(context, options){ 291 | if (contextOverride){ context = contextOverride; } 292 | callback.call(context, options); 293 | }); 294 | }, 295 | 296 | // Run all registered callbacks with the context specified. 297 | // Additional callbacks can be added after this has been run 298 | // and they will still be executed. 299 | run: function(options, context){ 300 | this._deferred.resolve(context, options); 301 | }, 302 | 303 | // Resets the list of callbacks to be run, allowing the same list 304 | // to be run multiple times - whenever the `run` method is called. 305 | reset: function(){ 306 | var that = this; 307 | var callbacks = this._callbacks; 308 | this._deferred = $.Deferred(); 309 | this._callbacks = []; 310 | _.each(callbacks, function(cb){ 311 | that.add(cb.cb, cb.ctx); 312 | }); 313 | } 314 | }); 315 | 316 | 317 | // Marionette Controller 318 | // --------------------- 319 | // 320 | // A multi-purpose object to use as a controller for 321 | // modules and routers, and as a mediator for workflow 322 | // and coordination of other objects, views, and more. 323 | Marionette.Controller = function(options){ 324 | this.triggerMethod = Marionette.triggerMethod; 325 | this.options = options || {}; 326 | 327 | if (_.isFunction(this.initialize)){ 328 | this.initialize(this.options); 329 | } 330 | }; 331 | 332 | Marionette.Controller.extend = Marionette.extend; 333 | 334 | // Controller Methods 335 | // -------------- 336 | 337 | // Ensure it can trigger events with Backbone.Events 338 | _.extend(Marionette.Controller.prototype, Backbone.Events, { 339 | close: function(){ 340 | this.stopListening(); 341 | this.triggerMethod("close"); 342 | this.unbind(); 343 | } 344 | }); 345 | 346 | // Region 347 | // ------ 348 | // 349 | // Manage the visual regions of your composite application. See 350 | // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ 351 | 352 | Marionette.Region = function(options){ 353 | this.options = options || {}; 354 | 355 | this.el = Marionette.getOption(this, "el"); 356 | 357 | if (!this.el){ 358 | var err = new Error("An 'el' must be specified for a region."); 359 | err.name = "NoElError"; 360 | throw err; 361 | } 362 | 363 | if (this.initialize){ 364 | var args = Array.prototype.slice.apply(arguments); 365 | this.initialize.apply(this, args); 366 | } 367 | }; 368 | 369 | 370 | // Region Type methods 371 | // ------------------- 372 | 373 | _.extend(Marionette.Region, { 374 | 375 | // Build an instance of a region by passing in a configuration object 376 | // and a default region type to use if none is specified in the config. 377 | // 378 | // The config object should either be a string as a jQuery DOM selector, 379 | // a Region type directly, or an object literal that specifies both 380 | // a selector and regionType: 381 | // 382 | // ```js 383 | // { 384 | // selector: "#foo", 385 | // regionType: MyCustomRegion 386 | // } 387 | // ``` 388 | // 389 | buildRegion: function(regionConfig, defaultRegionType){ 390 | var regionIsString = (typeof regionConfig === "string"); 391 | var regionSelectorIsString = (typeof regionConfig.selector === "string"); 392 | var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined"); 393 | var regionIsType = (typeof regionConfig === "function"); 394 | 395 | if (!regionIsType && !regionIsString && !regionSelectorIsString) { 396 | throw new Error("Region must be specified as a Region type, a selector string or an object with selector property"); 397 | } 398 | 399 | var selector, RegionType; 400 | 401 | // get the selector for the region 402 | 403 | if (regionIsString) { 404 | selector = regionConfig; 405 | } 406 | 407 | if (regionConfig.selector) { 408 | selector = regionConfig.selector; 409 | } 410 | 411 | // get the type for the region 412 | 413 | if (regionIsType){ 414 | RegionType = regionConfig; 415 | } 416 | 417 | if (!regionIsType && regionTypeIsUndefined) { 418 | RegionType = defaultRegionType; 419 | } 420 | 421 | if (regionConfig.regionType) { 422 | RegionType = regionConfig.regionType; 423 | } 424 | 425 | // build the region instance 426 | 427 | var regionManager = new RegionType({ 428 | el: selector 429 | }); 430 | 431 | return regionManager; 432 | } 433 | 434 | }); 435 | 436 | // Region Instance Methods 437 | // ----------------------- 438 | 439 | _.extend(Marionette.Region.prototype, Backbone.Events, { 440 | 441 | // Displays a backbone view instance inside of the region. 442 | // Handles calling the `render` method for you. Reads content 443 | // directly from the `el` attribute. Also calls an optional 444 | // `onShow` and `close` method on your view, just after showing 445 | // or just before closing the view, respectively. 446 | show: function(view){ 447 | 448 | this.ensureEl(); 449 | this.close(); 450 | 451 | view.render(); 452 | this.open(view); 453 | 454 | Marionette.triggerMethod.call(view, "show"); 455 | Marionette.triggerMethod.call(this, "show", view); 456 | 457 | this.currentView = view; 458 | }, 459 | 460 | ensureEl: function(){ 461 | if (!this.$el || this.$el.length === 0){ 462 | this.$el = this.getEl(this.el); 463 | } 464 | }, 465 | 466 | // Override this method to change how the region finds the 467 | // DOM element that it manages. Return a jQuery selector object. 468 | getEl: function(selector){ 469 | return $(selector); 470 | }, 471 | 472 | // Override this method to change how the new view is 473 | // appended to the `$el` that the region is managing 474 | open: function(view){ 475 | this.$el.empty().append(view.el); 476 | }, 477 | 478 | // Close the current view, if there is one. If there is no 479 | // current view, it does nothing and returns immediately. 480 | close: function(){ 481 | var view = this.currentView; 482 | if (!view || view.isClosed){ return; } 483 | 484 | if (view.close) { view.close(); } 485 | Marionette.triggerMethod.call(this, "close"); 486 | 487 | delete this.currentView; 488 | }, 489 | 490 | // Attach an existing view to the region. This 491 | // will not call `render` or `onShow` for the new view, 492 | // and will not replace the current HTML for the `el` 493 | // of the region. 494 | attachView: function(view){ 495 | this.currentView = view; 496 | }, 497 | 498 | // Reset the region by closing any existing view and 499 | // clearing out the cached `$el`. The next time a view 500 | // is shown via this region, the region will re-query the 501 | // DOM for the region's `el`. 502 | reset: function(){ 503 | this.close(); 504 | delete this.$el; 505 | } 506 | }); 507 | 508 | // Copy the `extend` function used by Backbone's classes 509 | Marionette.Region.extend = Marionette.extend; 510 | 511 | 512 | // Template Cache 513 | // -------------- 514 | 515 | // Manage templates stored in `