├── .gitignore ├── test ├── server.js └── index.html ├── package.json ├── README.md └── lib └── backbone-browserify.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | // node test/server.js 2 | // visit localhost:9000 in browser 3 | 4 | var express = require('express'), 5 | browserify = require('browserify'); 6 | var app = express.createServer(); 7 | 8 | app.set('views', __dirname + '/'); 9 | app.set('view options', { 10 | layout: false 11 | }); 12 | 13 | app.use(express.bodyParser()); 14 | app.use(express.methodOverride()); 15 | app.use(app.router); 16 | app.use(browserify({ 17 | require : { 18 | jQuery: 'jquery-browserify', 19 | backbone: __dirname + '/../lib/backbone-browserify.js' 20 | } 21 | })); 22 | 23 | app.get('/', function(req, res) { 24 | res.render('index.html'); 25 | }); 26 | 27 | app.listen(9000); -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Backbone Browserify test 6 | 7 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "backbone-browserify", 3 | "description" : "DEPRECATED, 0.9.9 works with browserify", 4 | "url" : "http://documentcloud.github.com/backbone/", 5 | "keywords" : ["util", "functional", "server", "client", "browser", "browserify", "backbone"], 6 | "author" : "Jeremy Ashkenas ", 7 | "contributors" : [], 8 | "dependencies" : { 9 | "underscore" : ">=1.1.2" 10 | }, 11 | "devDependencies": { 12 | "jquery-browserify": "~1.7", 13 | "express" : "~2", 14 | "browserify" : "*" 15 | }, 16 | "lib" : "lib", 17 | "main" : "lib/backbone-browserify.js", 18 | "repository" : "git://github.com/kmiyashiro/backbone-browserify.git", 19 | "version" : "0.9.2-1", 20 | "browserify" : { 21 | "dependencies" : { 22 | "underscore" : ">=1.1.2" 23 | }, 24 | "main" : "lib/backbone-browserify.js" 25 | } 26 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | ## Use Backbone 0.9.9=< 4 | 5 | Backbone has added exports support as of 0.9.9, so just use the normal Backbone package. 6 | 7 | To use Backbone with jQuery, remember to set `$` after you require Backbone. 8 | 9 | ```js 10 | var $ = require('jquery-browserify'); 11 | 12 | var Backbone = require('../lib/backbone-browserify'); 13 | Backbone.$ = $; 14 | 15 | MyView = Backbone.View.extend({ 16 | el: 'body', 17 | initialize: function() { 18 | this.render(); 19 | }, 20 | render: function() { 21 | $(this.el).html('

Oh hi

'); 22 | } 23 | }); 24 | 25 | new MyView(); 26 | ``` 27 | 28 | # Backbone-browserify 29 | ## packaged for use with [node-browserify](https://github.com/substack/node-browserify). 30 | 31 | ### Breaking change 0.9.2-1 32 | 33 | Removed require('jquery') in Backbone source for `$` assignment. Didn't make sense, see [#6](https://github.com/kmiyashiro/backbone-browserify/pull/6) 34 | 35 | ### Install 36 | 37 | ```bash 38 | npm install backbone-browserify 39 | ``` 40 | 41 | **Important:** You must require `jquery-browserify`, 'br-jquery', or Zepto (untested) with Browserify before you require Backbone, just like normal. 42 | 43 | Just add it to your browserify require list and use it! Make sure you also have Underscore installed via npm as Backbone will automatically require it. 44 | 45 | ### Server Side 46 | ````javascript 47 | browserify({ 48 | require : [ 'jquery-browserify', 'backbone-browserify' ] 49 | }); 50 | ```` 51 | 52 | ... or to alias it to just "backbone": 53 | 54 | ````javascript 55 | browserify({ 56 | require : { jquery: 'jquery-browserify', backbone: 'backbone-browserify' } 57 | }); 58 | ```` 59 | 60 | #### Express example 61 | ```js 62 | app.configure(function(){ 63 | app.set('views', __dirname + '/views'); 64 | app.set('view engine', 'jade'); 65 | app.use(express.bodyParser()); 66 | app.use(express.methodOverride()); 67 | app.use(app.router); 68 | app.use(express.static(__dirname + '/public')); 69 | app.use(browserify({ 70 | require : { jquery: 'jquery-browserify', backbone: 'backbone-browserify' } 71 | })); 72 | }); 73 | ``` 74 | 75 | ### Client Side 76 | 77 | ***Include `browserify.js` like this first: `` 78 | 79 | ````javascript 80 | var $ = jQuery = require('jquery-browserify'), 81 | Backbone = require('backbone-browserify'), 82 | MyView = Backbone.View.extend({ 83 | el: 'body', 84 | initialize: function() { 85 | this.render(); 86 | }, 87 | render: function() { 88 | $(this.el).html('

Oh hi

'); 89 | } 90 | }); 91 | 92 | $(document).ready(function() { var myView = new MyView(); }); 93 | ```` 94 | 95 | ... or if you aliased it to 'backbone': 96 | 97 | ````javascript 98 | var $ = jQuery = require('jquery'), 99 | Backbone = require('backbone'), 100 | MyView = Backbone.View.extend({ 101 | el: 'body', 102 | initialize: function() { 103 | this.render(); 104 | }, 105 | render: function() { 106 | $(this.el).html('

Oh hi

'); 107 | } 108 | }); 109 | 110 | $(document).ready(function() { var myView = new MyView(); }); 111 | ```` 112 | -------------------------------------------------------------------------------- /lib/backbone-browserify.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.9.2 2 | 3 | // (c) 2010-2012 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 | function create(){ 10 | 11 | // Initial Setup 12 | // ------------- 13 | 14 | // Save a reference to the global object (`window` in the browser, `global` 15 | // on the server). 16 | var root = this; 17 | 18 | // Save the previous value of the `Backbone` variable, so that it can be 19 | // restored later on, if `noConflict` is used. 20 | var previousBackbone = root.Backbone; 21 | 22 | // Create a local reference to slice/splice. 23 | var slice = Array.prototype.slice; 24 | var splice = Array.prototype.splice; 25 | 26 | // The top-level namespace. All public Backbone classes and modules will 27 | // be attached to this. Exported for both CommonJS and the browser. 28 | var Backbone; 29 | if (typeof exports !== 'undefined') { 30 | Backbone = exports; 31 | } else { 32 | Backbone = root.Backbone = {}; 33 | } 34 | 35 | // Current version of the library. Keep in sync with `package.json`. 36 | Backbone.VERSION = '0.9.2'; 37 | 38 | // Require Underscore, if we're on the server, and it's not already present. 39 | var _ = root._; 40 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); 41 | 42 | // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. 43 | var $ = root.jQuery || root.Zepto || root.ender; 44 | 45 | // Set the JavaScript library that will be used for DOM manipulation and 46 | // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery, 47 | // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an 48 | // alternate JavaScript library (or a mock library for testing your views 49 | // outside of a browser). 50 | Backbone.setDomLibrary = function(lib) { 51 | $ = lib; 52 | }; 53 | 54 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 55 | // to its previous owner. Returns a reference to this Backbone object. 56 | Backbone.noConflict = function() { 57 | root.Backbone = previousBackbone; 58 | return this; 59 | }; 60 | 61 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 62 | // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and 63 | // set a `X-Http-Method-Override` header. 64 | Backbone.emulateHTTP = false; 65 | 66 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 67 | // `application/json` requests ... will encode the body as 68 | // `application/x-www-form-urlencoded` instead and will send the model in a 69 | // form param named `model`. 70 | Backbone.emulateJSON = false; 71 | 72 | // Backbone.Events 73 | // ----------------- 74 | 75 | // Regular expression used to split event strings 76 | var eventSplitter = /\s+/; 77 | 78 | // A module that can be mixed in to *any object* in order to provide it with 79 | // custom events. You may bind with `on` or remove with `off` callback functions 80 | // to an event; trigger`-ing an event fires all callbacks in succession. 81 | // 82 | // var object = {}; 83 | // _.extend(object, Backbone.Events); 84 | // object.on('expand', function(){ alert('expanded'); }); 85 | // object.trigger('expand'); 86 | // 87 | var Events = Backbone.Events = { 88 | 89 | // Bind one or more space separated events, `events`, to a `callback` 90 | // function. Passing `"all"` will bind the callback to all events fired. 91 | on: function(events, callback, context) { 92 | 93 | var calls, event, node, tail, list; 94 | if (!callback) return this; 95 | events = events.split(eventSplitter); 96 | calls = this._callbacks || (this._callbacks = {}); 97 | 98 | // Create an immutable callback list, allowing traversal during 99 | // modification. The tail is an empty object that will always be used 100 | // as the next node. 101 | while (event = events.shift()) { 102 | list = calls[event]; 103 | node = list ? list.tail : {}; 104 | node.next = tail = {}; 105 | node.context = context; 106 | node.callback = callback; 107 | calls[event] = {tail: tail, next: list ? list.next : node}; 108 | } 109 | 110 | return this; 111 | }, 112 | 113 | // Remove one or many callbacks. If `context` is null, removes all callbacks 114 | // with that function. If `callback` is null, removes all callbacks for the 115 | // event. If `events` is null, removes all bound callbacks for all events. 116 | off: function(events, callback, context) { 117 | var event, calls, node, tail, cb, ctx; 118 | 119 | // No events, or removing *all* events. 120 | if (!(calls = this._callbacks)) return; 121 | if (!(events || callback || context)) { 122 | delete this._callbacks; 123 | return this; 124 | } 125 | 126 | // Loop through the listed events and contexts, splicing them out of the 127 | // linked list of callbacks if appropriate. 128 | events = events ? events.split(eventSplitter) : _.keys(calls); 129 | while (event = events.shift()) { 130 | node = calls[event]; 131 | delete calls[event]; 132 | if (!node || !(callback || context)) continue; 133 | // Create a new list, omitting the indicated callbacks. 134 | tail = node.tail; 135 | while ((node = node.next) !== tail) { 136 | cb = node.callback; 137 | ctx = node.context; 138 | if ((callback && cb !== callback) || (context && ctx !== context)) { 139 | this.on(event, cb, ctx); 140 | } 141 | } 142 | } 143 | 144 | return this; 145 | }, 146 | 147 | // Trigger one or many events, firing all bound callbacks. Callbacks are 148 | // passed the same arguments as `trigger` is, apart from the event name 149 | // (unless you're listening on `"all"`, which will cause your callback to 150 | // receive the true name of the event as the first argument). 151 | trigger: function(events) { 152 | var event, node, calls, tail, args, all, rest; 153 | if (!(calls = this._callbacks)) return this; 154 | all = calls.all; 155 | events = events.split(eventSplitter); 156 | rest = slice.call(arguments, 1); 157 | 158 | // For each event, walk through the linked list of callbacks twice, 159 | // first to trigger the event, then to trigger any `"all"` callbacks. 160 | while (event = events.shift()) { 161 | if (node = calls[event]) { 162 | tail = node.tail; 163 | while ((node = node.next) !== tail) { 164 | node.callback.apply(node.context || this, rest); 165 | } 166 | } 167 | if (node = all) { 168 | tail = node.tail; 169 | args = [event].concat(rest); 170 | while ((node = node.next) !== tail) { 171 | node.callback.apply(node.context || this, args); 172 | } 173 | } 174 | } 175 | 176 | return this; 177 | } 178 | 179 | }; 180 | 181 | // Aliases for backwards compatibility. 182 | Events.bind = Events.on; 183 | Events.unbind = Events.off; 184 | 185 | // Backbone.Model 186 | // -------------- 187 | 188 | // Create a new model, with defined attributes. A client id (`cid`) 189 | // is automatically generated and assigned for you. 190 | var Model = Backbone.Model = function(attributes, options) { 191 | var defaults; 192 | attributes || (attributes = {}); 193 | if (options && options.parse) attributes = this.parse(attributes); 194 | if (defaults = getValue(this, 'defaults')) { 195 | attributes = _.extend({}, defaults, attributes); 196 | } 197 | if (options && options.collection) this.collection = options.collection; 198 | this.attributes = {}; 199 | this._escapedAttributes = {}; 200 | this.cid = _.uniqueId('c'); 201 | this.changed = {}; 202 | this._silent = {}; 203 | this._pending = {}; 204 | this.set(attributes, {silent: true}); 205 | // Reset change tracking. 206 | this.changed = {}; 207 | this._silent = {}; 208 | this._pending = {}; 209 | this._previousAttributes = _.clone(this.attributes); 210 | this.initialize.apply(this, arguments); 211 | }; 212 | 213 | // Attach all inheritable methods to the Model prototype. 214 | _.extend(Model.prototype, Events, { 215 | 216 | // A hash of attributes whose current and previous value differ. 217 | changed: null, 218 | 219 | // A hash of attributes that have silently changed since the last time 220 | // `change` was called. Will become pending attributes on the next call. 221 | _silent: null, 222 | 223 | // A hash of attributes that have changed since the last `'change'` event 224 | // began. 225 | _pending: null, 226 | 227 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 228 | // CouchDB users may want to set this to `"_id"`. 229 | idAttribute: 'id', 230 | 231 | // Initialize is an empty function by default. Override it with your own 232 | // initialization logic. 233 | initialize: function(){}, 234 | 235 | // Return a copy of the model's `attributes` object. 236 | toJSON: function(options) { 237 | return _.clone(this.attributes); 238 | }, 239 | 240 | // Get the value of an attribute. 241 | get: function(attr) { 242 | return this.attributes[attr]; 243 | }, 244 | 245 | // Get the HTML-escaped value of an attribute. 246 | escape: function(attr) { 247 | var html; 248 | if (html = this._escapedAttributes[attr]) return html; 249 | var val = this.get(attr); 250 | return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); 251 | }, 252 | 253 | // Returns `true` if the attribute contains a value that is not null 254 | // or undefined. 255 | has: function(attr) { 256 | return this.get(attr) != null; 257 | }, 258 | 259 | // Set a hash of model attributes on the object, firing `"change"` unless 260 | // you choose to silence it. 261 | set: function(key, value, options) { 262 | var attrs, attr, val; 263 | 264 | // Handle both 265 | if (_.isObject(key) || key == null) { 266 | attrs = key; 267 | options = value; 268 | } else { 269 | attrs = {}; 270 | attrs[key] = value; 271 | } 272 | 273 | // Extract attributes and options. 274 | options || (options = {}); 275 | if (!attrs) return this; 276 | if (attrs instanceof Model) attrs = attrs.attributes; 277 | if (options.unset) for (attr in attrs) attrs[attr] = void 0; 278 | 279 | // Run validation. 280 | if (!this._validate(attrs, options)) return false; 281 | 282 | // Check for changes of `id`. 283 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 284 | 285 | var changes = options.changes = {}; 286 | var now = this.attributes; 287 | var escaped = this._escapedAttributes; 288 | var prev = this._previousAttributes || {}; 289 | 290 | // For each `set` attribute... 291 | for (attr in attrs) { 292 | val = attrs[attr]; 293 | 294 | // If the new and current value differ, record the change. 295 | if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) { 296 | delete escaped[attr]; 297 | (options.silent ? this._silent : changes)[attr] = true; 298 | } 299 | 300 | // Update or delete the current value. 301 | options.unset ? delete now[attr] : now[attr] = val; 302 | 303 | // If the new and previous value differ, record the change. If not, 304 | // then remove changes for this attribute. 305 | if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) { 306 | this.changed[attr] = val; 307 | if (!options.silent) this._pending[attr] = true; 308 | } else { 309 | delete this.changed[attr]; 310 | delete this._pending[attr]; 311 | } 312 | } 313 | 314 | // Fire the `"change"` events. 315 | if (!options.silent) this.change(options); 316 | return this; 317 | }, 318 | 319 | // Remove an attribute from the model, firing `"change"` unless you choose 320 | // to silence it. `unset` is a noop if the attribute doesn't exist. 321 | unset: function(attr, options) { 322 | (options || (options = {})).unset = true; 323 | return this.set(attr, null, options); 324 | }, 325 | 326 | // Clear all attributes on the model, firing `"change"` unless you choose 327 | // to silence it. 328 | clear: function(options) { 329 | (options || (options = {})).unset = true; 330 | return this.set(_.clone(this.attributes), options); 331 | }, 332 | 333 | // Fetch the model from the server. If the server's representation of the 334 | // model differs from its current attributes, they will be overriden, 335 | // triggering a `"change"` event. 336 | fetch: function(options) { 337 | options = options ? _.clone(options) : {}; 338 | var model = this; 339 | var success = options.success; 340 | options.success = function(resp, status, xhr) { 341 | if (!model.set(model.parse(resp, xhr), options)) return false; 342 | if (success) success(model, resp); 343 | }; 344 | options.error = Backbone.wrapError(options.error, model, options); 345 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 346 | }, 347 | 348 | // Set a hash of model attributes, and sync the model to the server. 349 | // If the server returns an attributes hash that differs, the model's 350 | // state will be `set` again. 351 | save: function(key, value, options) { 352 | var attrs, current; 353 | 354 | // Handle both `("key", value)` and `({key: value})` -style calls. 355 | if (_.isObject(key) || key == null) { 356 | attrs = key; 357 | options = value; 358 | } else { 359 | attrs = {}; 360 | attrs[key] = value; 361 | } 362 | options = options ? _.clone(options) : {}; 363 | 364 | // If we're "wait"-ing to set changed attributes, validate early. 365 | if (options.wait) { 366 | if (!this._validate(attrs, options)) return false; 367 | current = _.clone(this.attributes); 368 | } 369 | 370 | // Regular saves `set` attributes before persisting to the server. 371 | var silentOptions = _.extend({}, options, {silent: true}); 372 | if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { 373 | return false; 374 | } 375 | 376 | // After a successful server-side save, the client is (optionally) 377 | // updated with the server-side state. 378 | var model = this; 379 | var success = options.success; 380 | options.success = function(resp, status, xhr) { 381 | var serverAttrs = model.parse(resp, xhr); 382 | if (options.wait) { 383 | delete options.wait; 384 | serverAttrs = _.extend(attrs || {}, serverAttrs); 385 | } 386 | if (!model.set(serverAttrs, options)) return false; 387 | if (success) { 388 | success(model, resp); 389 | } else { 390 | model.trigger('sync', model, resp, options); 391 | } 392 | }; 393 | 394 | // Finish configuring and sending the Ajax request. 395 | options.error = Backbone.wrapError(options.error, model, options); 396 | var method = this.isNew() ? 'create' : 'update'; 397 | var xhr = (this.sync || Backbone.sync).call(this, method, this, options); 398 | if (options.wait) this.set(current, silentOptions); 399 | return xhr; 400 | }, 401 | 402 | // Destroy this model on the server if it was already persisted. 403 | // Optimistically removes the model from its collection, if it has one. 404 | // If `wait: true` is passed, waits for the server to respond before removal. 405 | destroy: function(options) { 406 | options = options ? _.clone(options) : {}; 407 | var model = this; 408 | var success = options.success; 409 | 410 | var triggerDestroy = function() { 411 | model.trigger('destroy', model, model.collection, options); 412 | }; 413 | 414 | if (this.isNew()) { 415 | triggerDestroy(); 416 | return false; 417 | } 418 | 419 | options.success = function(resp) { 420 | if (options.wait) triggerDestroy(); 421 | if (success) { 422 | success(model, resp); 423 | } else { 424 | model.trigger('sync', model, resp, options); 425 | } 426 | }; 427 | 428 | options.error = Backbone.wrapError(options.error, model, options); 429 | var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); 430 | if (!options.wait) triggerDestroy(); 431 | return xhr; 432 | }, 433 | 434 | // Default URL for the model's representation on the server -- if you're 435 | // using Backbone's restful methods, override this to change the endpoint 436 | // that will be called. 437 | url: function() { 438 | var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError(); 439 | if (this.isNew()) return base; 440 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); 441 | }, 442 | 443 | // **parse** converts a response into the hash of attributes to be `set` on 444 | // the model. The default implementation is just to pass the response along. 445 | parse: function(resp, xhr) { 446 | return resp; 447 | }, 448 | 449 | // Create a new model with identical attributes to this one. 450 | clone: function() { 451 | return new this.constructor(this.attributes); 452 | }, 453 | 454 | // A model is new if it has never been saved to the server, and lacks an id. 455 | isNew: function() { 456 | return this.id == null; 457 | }, 458 | 459 | // Call this method to manually fire a `"change"` event for this model and 460 | // a `"change:attribute"` event for each changed attribute. 461 | // Calling this will cause all objects observing the model to update. 462 | change: function(options) { 463 | options || (options = {}); 464 | var changing = this._changing; 465 | this._changing = true; 466 | 467 | // Silent changes become pending changes. 468 | for (var attr in this._silent) this._pending[attr] = true; 469 | 470 | // Silent changes are triggered. 471 | var changes = _.extend({}, options.changes, this._silent); 472 | this._silent = {}; 473 | for (var attr in changes) { 474 | this.trigger('change:' + attr, this, this.get(attr), options); 475 | } 476 | if (changing) return this; 477 | 478 | // Continue firing `"change"` events while there are pending changes. 479 | while (!_.isEmpty(this._pending)) { 480 | this._pending = {}; 481 | this.trigger('change', this, options); 482 | // Pending and silent changes still remain. 483 | for (var attr in this.changed) { 484 | if (this._pending[attr] || this._silent[attr]) continue; 485 | delete this.changed[attr]; 486 | } 487 | this._previousAttributes = _.clone(this.attributes); 488 | } 489 | 490 | this._changing = false; 491 | return this; 492 | }, 493 | 494 | // Determine if the model has changed since the last `"change"` event. 495 | // If you specify an attribute name, determine if that attribute has changed. 496 | hasChanged: function(attr) { 497 | if (!arguments.length) return !_.isEmpty(this.changed); 498 | return _.has(this.changed, attr); 499 | }, 500 | 501 | // Return an object containing all the attributes that have changed, or 502 | // false if there are no changed attributes. Useful for determining what 503 | // parts of a view need to be updated and/or what attributes need to be 504 | // persisted to the server. Unset attributes will be set to undefined. 505 | // You can also pass an attributes object to diff against the model, 506 | // determining if there *would be* a change. 507 | changedAttributes: function(diff) { 508 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 509 | var val, changed = false, old = this._previousAttributes; 510 | for (var attr in diff) { 511 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 512 | (changed || (changed = {}))[attr] = val; 513 | } 514 | return changed; 515 | }, 516 | 517 | // Get the previous value of an attribute, recorded at the time the last 518 | // `"change"` event was fired. 519 | previous: function(attr) { 520 | if (!arguments.length || !this._previousAttributes) return null; 521 | return this._previousAttributes[attr]; 522 | }, 523 | 524 | // Get all of the attributes of the model at the time of the previous 525 | // `"change"` event. 526 | previousAttributes: function() { 527 | return _.clone(this._previousAttributes); 528 | }, 529 | 530 | // Check if the model is currently in a valid state. It's only possible to 531 | // get into an *invalid* state if you're using silent changes. 532 | isValid: function() { 533 | return !this.validate(this.attributes); 534 | }, 535 | 536 | // Run validation against the next complete set of model attributes, 537 | // returning `true` if all is well. If a specific `error` callback has 538 | // been passed, call that instead of firing the general `"error"` event. 539 | _validate: function(attrs, options) { 540 | if (options.silent || !this.validate) return true; 541 | attrs = _.extend({}, this.attributes, attrs); 542 | var error = this.validate(attrs, options); 543 | if (!error) return true; 544 | if (options && options.error) { 545 | options.error(this, error, options); 546 | } else { 547 | this.trigger('error', this, error, options); 548 | } 549 | return false; 550 | } 551 | 552 | }); 553 | 554 | // Backbone.Collection 555 | // ------------------- 556 | 557 | // Provides a standard collection class for our sets of models, ordered 558 | // or unordered. If a `comparator` is specified, the Collection will maintain 559 | // its models in sort order, as they're added and removed. 560 | var Collection = Backbone.Collection = function(models, options) { 561 | options || (options = {}); 562 | if (options.model) this.model = options.model; 563 | if (options.comparator) this.comparator = options.comparator; 564 | this._reset(); 565 | this.initialize.apply(this, arguments); 566 | if (models) this.reset(models, {silent: true, parse: options.parse}); 567 | }; 568 | 569 | // Define the Collection's inheritable methods. 570 | _.extend(Collection.prototype, Events, { 571 | 572 | // The default model for a collection is just a **Backbone.Model**. 573 | // This should be overridden in most cases. 574 | model: Model, 575 | 576 | // Initialize is an empty function by default. Override it with your own 577 | // initialization logic. 578 | initialize: function(){}, 579 | 580 | // The JSON representation of a Collection is an array of the 581 | // models' attributes. 582 | toJSON: function(options) { 583 | return this.map(function(model){ return model.toJSON(options); }); 584 | }, 585 | 586 | // Add a model, or list of models to the set. Pass **silent** to avoid 587 | // firing the `add` event for every new model. 588 | add: function(models, options) { 589 | var i, index, length, model, cid, id, cids = {}, ids = {}, dups = []; 590 | options || (options = {}); 591 | models = _.isArray(models) ? models.slice() : [models]; 592 | 593 | // Begin by turning bare objects into model references, and preventing 594 | // invalid models or duplicate models from being added. 595 | for (i = 0, length = models.length; i < length; i++) { 596 | if (!(model = models[i] = this._prepareModel(models[i], options))) { 597 | throw new Error("Can't add an invalid model to a collection"); 598 | } 599 | cid = model.cid; 600 | id = model.id; 601 | if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) { 602 | dups.push(i); 603 | continue; 604 | } 605 | cids[cid] = ids[id] = model; 606 | } 607 | 608 | // Remove duplicates. 609 | i = dups.length; 610 | while (i--) { 611 | models.splice(dups[i], 1); 612 | } 613 | 614 | // Listen to added models' events, and index models for lookup by 615 | // `id` and by `cid`. 616 | for (i = 0, length = models.length; i < length; i++) { 617 | (model = models[i]).on('all', this._onModelEvent, this); 618 | this._byCid[model.cid] = model; 619 | if (model.id != null) this._byId[model.id] = model; 620 | } 621 | 622 | // Insert models into the collection, re-sorting if needed, and triggering 623 | // `add` events unless silenced. 624 | this.length += length; 625 | index = options.at != null ? options.at : this.models.length; 626 | splice.apply(this.models, [index, 0].concat(models)); 627 | if (this.comparator) this.sort({silent: true}); 628 | if (options.silent) return this; 629 | for (i = 0, length = this.models.length; i < length; i++) { 630 | if (!cids[(model = this.models[i]).cid]) continue; 631 | options.index = i; 632 | model.trigger('add', model, this, options); 633 | } 634 | return this; 635 | }, 636 | 637 | // Remove a model, or a list of models from the set. Pass silent to avoid 638 | // firing the `remove` event for every model removed. 639 | remove: function(models, options) { 640 | var i, l, index, model; 641 | options || (options = {}); 642 | models = _.isArray(models) ? models.slice() : [models]; 643 | for (i = 0, l = models.length; i < l; i++) { 644 | model = this.getByCid(models[i]) || this.get(models[i]); 645 | if (!model) continue; 646 | delete this._byId[model.id]; 647 | delete this._byCid[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 | // Add a model to the end of the collection. 661 | push: function(model, options) { 662 | model = this._prepareModel(model, options); 663 | this.add(model, options); 664 | return model; 665 | }, 666 | 667 | // Remove a model from the end of the collection. 668 | pop: function(options) { 669 | var model = this.at(this.length - 1); 670 | this.remove(model, options); 671 | return model; 672 | }, 673 | 674 | // Add a model to the beginning of the collection. 675 | unshift: function(model, options) { 676 | model = this._prepareModel(model, options); 677 | this.add(model, _.extend({at: 0}, options)); 678 | return model; 679 | }, 680 | 681 | // Remove a model from the beginning of the collection. 682 | shift: function(options) { 683 | var model = this.at(0); 684 | this.remove(model, options); 685 | return model; 686 | }, 687 | 688 | // Get a model from the set by id. 689 | get: function(id) { 690 | if (id == null) return void 0; 691 | return this._byId[id.id != null ? id.id : id]; 692 | }, 693 | 694 | // Get a model from the set by client id. 695 | getByCid: function(cid) { 696 | return cid && this._byCid[cid.cid || cid]; 697 | }, 698 | 699 | // Get the model at the given index. 700 | at: function(index) { 701 | return this.models[index]; 702 | }, 703 | 704 | // Return models with matching attributes. Useful for simple cases of `filter`. 705 | where: function(attrs) { 706 | if (_.isEmpty(attrs)) return []; 707 | return this.filter(function(model) { 708 | for (var key in attrs) { 709 | if (attrs[key] !== model.get(key)) return false; 710 | } 711 | return true; 712 | }); 713 | }, 714 | 715 | // Force the collection to re-sort itself. You don't need to call this under 716 | // normal circumstances, as the set will maintain sort order as each item 717 | // is added. 718 | sort: function(options) { 719 | options || (options = {}); 720 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 721 | var boundComparator = _.bind(this.comparator, this); 722 | if (this.comparator.length == 1) { 723 | this.models = this.sortBy(boundComparator); 724 | } else { 725 | this.models.sort(boundComparator); 726 | } 727 | if (!options.silent) this.trigger('reset', this, options); 728 | return this; 729 | }, 730 | 731 | // Pluck an attribute from each model in the collection. 732 | pluck: function(attr) { 733 | return _.map(this.models, function(model){ return model.get(attr); }); 734 | }, 735 | 736 | // When you have more items than you want to add or remove individually, 737 | // you can reset the entire set with a new list of models, without firing 738 | // any `add` or `remove` events. Fires `reset` when finished. 739 | reset: function(models, options) { 740 | models || (models = []); 741 | options || (options = {}); 742 | for (var i = 0, l = this.models.length; i < l; i++) { 743 | this._removeReference(this.models[i]); 744 | } 745 | this._reset(); 746 | this.add(models, _.extend({silent: true}, options)); 747 | if (!options.silent) this.trigger('reset', this, options); 748 | return this; 749 | }, 750 | 751 | // Fetch the default set of models for this collection, resetting the 752 | // collection when they arrive. If `add: true` is passed, appends the 753 | // models to the collection instead of resetting. 754 | fetch: function(options) { 755 | options = options ? _.clone(options) : {}; 756 | if (options.parse === undefined) options.parse = true; 757 | var collection = this; 758 | var success = options.success; 759 | options.success = function(resp, status, xhr) { 760 | collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); 761 | if (success) success(collection, resp); 762 | }; 763 | options.error = Backbone.wrapError(options.error, collection, options); 764 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 765 | }, 766 | 767 | // Create a new instance of a model in this collection. Add the model to the 768 | // collection immediately, unless `wait: true` is passed, in which case we 769 | // wait for the server to agree. 770 | create: function(model, options) { 771 | var coll = this; 772 | options = options ? _.clone(options) : {}; 773 | model = this._prepareModel(model, options); 774 | if (!model) return false; 775 | if (!options.wait) coll.add(model, options); 776 | var success = options.success; 777 | options.success = function(nextModel, resp, xhr) { 778 | if (options.wait) coll.add(nextModel, options); 779 | if (success) { 780 | success(nextModel, resp); 781 | } else { 782 | nextModel.trigger('sync', model, resp, options); 783 | } 784 | }; 785 | model.save(null, options); 786 | return model; 787 | }, 788 | 789 | // **parse** converts a response into a list of models to be added to the 790 | // collection. The default implementation is just to pass it through. 791 | parse: function(resp, xhr) { 792 | return resp; 793 | }, 794 | 795 | // Proxy to _'s chain. Can't be proxied the same way the rest of the 796 | // underscore methods are proxied because it relies on the underscore 797 | // constructor. 798 | chain: function () { 799 | return _(this.models).chain(); 800 | }, 801 | 802 | // Reset all internal state. Called when the collection is reset. 803 | _reset: function(options) { 804 | this.length = 0; 805 | this.models = []; 806 | this._byId = {}; 807 | this._byCid = {}; 808 | }, 809 | 810 | // Prepare a model or hash of attributes to be added to this collection. 811 | _prepareModel: function(model, options) { 812 | options || (options = {}); 813 | if (!(model instanceof Model)) { 814 | var attrs = model; 815 | options.collection = this; 816 | model = new this.model(attrs, options); 817 | if (!model._validate(model.attributes, options)) model = false; 818 | } else if (!model.collection) { 819 | model.collection = this; 820 | } 821 | return model; 822 | }, 823 | 824 | // Internal method to remove a model's ties to a collection. 825 | _removeReference: function(model) { 826 | if (this == model.collection) { 827 | delete model.collection; 828 | } 829 | model.off('all', this._onModelEvent, this); 830 | }, 831 | 832 | // Internal method called every time a model in the set fires an event. 833 | // Sets need to update their indexes when models change ids. All other 834 | // events simply proxy through. "add" and "remove" events that originate 835 | // in other collections are ignored. 836 | _onModelEvent: function(event, model, collection, options) { 837 | if ((event == 'add' || event == 'remove') && collection != this) return; 838 | if (event == 'destroy') { 839 | this.remove(model, options); 840 | } 841 | if (model && event === 'change:' + model.idAttribute) { 842 | delete this._byId[model.previous(model.idAttribute)]; 843 | this._byId[model.id] = model; 844 | } 845 | this.trigger.apply(this, arguments); 846 | } 847 | 848 | }); 849 | 850 | // Underscore methods that we want to implement on the Collection. 851 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 852 | 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 853 | 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 854 | 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', 855 | 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; 856 | 857 | // Mix in each Underscore method as a proxy to `Collection#models`. 858 | _.each(methods, function(method) { 859 | Collection.prototype[method] = function() { 860 | return _[method].apply(_, [this.models].concat(_.toArray(arguments))); 861 | }; 862 | }); 863 | 864 | // Backbone.Router 865 | // ------------------- 866 | 867 | // Routers map faux-URLs to actions, and fire events when routes are 868 | // matched. Creating a new one sets its `routes` hash, if not set statically. 869 | var Router = Backbone.Router = function(options) { 870 | options || (options = {}); 871 | if (options.routes) this.routes = options.routes; 872 | this._bindRoutes(); 873 | this.initialize.apply(this, arguments); 874 | }; 875 | 876 | // Cached regular expressions for matching named param parts and splatted 877 | // parts of route strings. 878 | var namedParam = /:\w+/g; 879 | var splatParam = /\*\w+/g; 880 | var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; 881 | 882 | // Set up all inheritable **Backbone.Router** properties and methods. 883 | _.extend(Router.prototype, Events, { 884 | 885 | // Initialize is an empty function by default. Override it with your own 886 | // initialization logic. 887 | initialize: function(){}, 888 | 889 | // Manually bind a single named route to a callback. For example: 890 | // 891 | // this.route('search/:query/p:num', 'search', function(query, num) { 892 | // ... 893 | // }); 894 | // 895 | route: function(route, name, callback) { 896 | Backbone.history || (Backbone.history = new History); 897 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 898 | if (!callback) callback = this[name]; 899 | Backbone.history.route(route, _.bind(function(fragment) { 900 | var args = this._extractParameters(route, fragment); 901 | callback && callback.apply(this, args); 902 | this.trigger.apply(this, ['route:' + name].concat(args)); 903 | Backbone.history.trigger('route', this, name, args); 904 | }, this)); 905 | return this; 906 | }, 907 | 908 | // Simple proxy to `Backbone.history` to save a fragment into the history. 909 | navigate: function(fragment, options) { 910 | Backbone.history.navigate(fragment, options); 911 | }, 912 | 913 | // Bind all defined routes to `Backbone.history`. We have to reverse the 914 | // order of the routes here to support behavior where the most general 915 | // routes can be defined at the bottom of the route map. 916 | _bindRoutes: function() { 917 | if (!this.routes) return; 918 | var routes = []; 919 | for (var route in this.routes) { 920 | routes.unshift([route, this.routes[route]]); 921 | } 922 | for (var i = 0, l = routes.length; i < l; i++) { 923 | this.route(routes[i][0], routes[i][1], this[routes[i][1]]); 924 | } 925 | }, 926 | 927 | // Convert a route string into a regular expression, suitable for matching 928 | // against the current location hash. 929 | _routeToRegExp: function(route) { 930 | route = route.replace(escapeRegExp, '\\$&') 931 | .replace(namedParam, '([^\/]+)') 932 | .replace(splatParam, '(.*?)'); 933 | return new RegExp('^' + route + '$'); 934 | }, 935 | 936 | // Given a route, and a URL fragment that it matches, return the array of 937 | // extracted parameters. 938 | _extractParameters: function(route, fragment) { 939 | return route.exec(fragment).slice(1); 940 | } 941 | 942 | }); 943 | 944 | // Backbone.History 945 | // ---------------- 946 | 947 | // Handles cross-browser history management, based on URL fragments. If the 948 | // browser does not support `onhashchange`, falls back to polling. 949 | var History = Backbone.History = function() { 950 | this.handlers = []; 951 | _.bindAll(this, 'checkUrl'); 952 | }; 953 | 954 | // Cached regex for cleaning leading hashes and slashes . 955 | var routeStripper = /^[#\/]/; 956 | 957 | // Cached regex for detecting MSIE. 958 | var isExplorer = /msie [\w.]+/; 959 | 960 | // Has the history handling already been started? 961 | History.started = false; 962 | 963 | // Set up all inheritable **Backbone.History** properties and methods. 964 | _.extend(History.prototype, Events, { 965 | 966 | // The default interval to poll for hash changes, if necessary, is 967 | // twenty times a second. 968 | interval: 50, 969 | 970 | // Gets the true hash value. Cannot use location.hash directly due to bug 971 | // in Firefox where location.hash will always be decoded. 972 | getHash: function(windowOverride) { 973 | var loc = windowOverride ? windowOverride.location : window.location; 974 | var match = loc.href.match(/#(.*)$/); 975 | return match ? match[1] : ''; 976 | }, 977 | 978 | // Get the cross-browser normalized URL fragment, either from the URL, 979 | // the hash, or the override. 980 | getFragment: function(fragment, forcePushState) { 981 | if (fragment == null) { 982 | if (this._hasPushState || forcePushState) { 983 | fragment = window.location.pathname; 984 | var search = window.location.search; 985 | if (search) fragment += search; 986 | } else { 987 | fragment = this.getHash(); 988 | } 989 | } 990 | if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length); 991 | return fragment.replace(routeStripper, ''); 992 | }, 993 | 994 | // Start the hash change handling, returning `true` if the current URL matches 995 | // an existing route, and `false` otherwise. 996 | start: function(options) { 997 | if (History.started) throw new Error("Backbone.history has already been started"); 998 | History.started = true; 999 | 1000 | // Figure out the initial configuration. Do we need an iframe? 1001 | // Is pushState desired ... is it available? 1002 | this.options = _.extend({}, {root: '/'}, this.options, options); 1003 | this._wantsHashChange = this.options.hashChange !== false; 1004 | this._wantsPushState = !!this.options.pushState; 1005 | this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); 1006 | var fragment = this.getFragment(); 1007 | var docMode = document.documentMode; 1008 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1009 | 1010 | if (oldIE) { 1011 | this.iframe = $('