├── .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 = $('').hide().appendTo('body')[0].contentWindow;
1012 | this.navigate(fragment);
1013 | }
1014 |
1015 | // Depending on whether we're using pushState or hashes, and whether
1016 | // 'onhashchange' is supported, determine how we check the URL state.
1017 | if (this._hasPushState) {
1018 | $(window).bind('popstate', this.checkUrl);
1019 | } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1020 | $(window).bind('hashchange', this.checkUrl);
1021 | } else if (this._wantsHashChange) {
1022 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1023 | }
1024 |
1025 | // Determine if we need to change the base url, for a pushState link
1026 | // opened by a non-pushState browser.
1027 | this.fragment = fragment;
1028 | var loc = window.location;
1029 | var atRoot = loc.pathname == this.options.root;
1030 |
1031 | // If we've started off with a route from a `pushState`-enabled browser,
1032 | // but we're currently in a browser that doesn't support it...
1033 | if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1034 | this.fragment = this.getFragment(null, true);
1035 | window.location.replace(this.options.root + '#' + this.fragment);
1036 | // Return immediately as browser will do redirect to new url
1037 | return true;
1038 |
1039 | // Or if we've started out with a hash-based route, but we're currently
1040 | // in a browser where it could be `pushState`-based instead...
1041 | } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1042 | this.fragment = this.getHash().replace(routeStripper, '');
1043 | window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
1044 | }
1045 |
1046 | if (!this.options.silent) {
1047 | return this.loadUrl();
1048 | }
1049 | },
1050 |
1051 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1052 | // but possibly useful for unit testing Routers.
1053 | stop: function() {
1054 | $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
1055 | clearInterval(this._checkUrlInterval);
1056 | History.started = false;
1057 | },
1058 |
1059 | // Add a route to be tested when the fragment changes. Routes added later
1060 | // may override previous routes.
1061 | route: function(route, callback) {
1062 | this.handlers.unshift({route: route, callback: callback});
1063 | },
1064 |
1065 | // Checks the current URL to see if it has changed, and if it has,
1066 | // calls `loadUrl`, normalizing across the hidden iframe.
1067 | checkUrl: function(e) {
1068 | var current = this.getFragment();
1069 | if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
1070 | if (current == this.fragment) return false;
1071 | if (this.iframe) this.navigate(current);
1072 | this.loadUrl() || this.loadUrl(this.getHash());
1073 | },
1074 |
1075 | // Attempt to load the current URL fragment. If a route succeeds with a
1076 | // match, returns `true`. If no defined routes matches the fragment,
1077 | // returns `false`.
1078 | loadUrl: function(fragmentOverride) {
1079 | var fragment = this.fragment = this.getFragment(fragmentOverride);
1080 | var matched = _.any(this.handlers, function(handler) {
1081 | if (handler.route.test(fragment)) {
1082 | handler.callback(fragment);
1083 | return true;
1084 | }
1085 | });
1086 | return matched;
1087 | },
1088 |
1089 | // Save a fragment into the hash history, or replace the URL state if the
1090 | // 'replace' option is passed. You are responsible for properly URL-encoding
1091 | // the fragment in advance.
1092 | //
1093 | // The options object can contain `trigger: true` if you wish to have the
1094 | // route callback be fired (not usually desirable), or `replace: true`, if
1095 | // you wish to modify the current URL without adding an entry to the history.
1096 | navigate: function(fragment, options) {
1097 | if (!History.started) return false;
1098 | if (!options || options === true) options = {trigger: options};
1099 | var frag = (fragment || '').replace(routeStripper, '');
1100 | if (this.fragment == frag) return;
1101 |
1102 | // If pushState is available, we use it to set the fragment as a real URL.
1103 | if (this._hasPushState) {
1104 | if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
1105 | this.fragment = frag;
1106 | window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
1107 |
1108 | // If hash changes haven't been explicitly disabled, update the hash
1109 | // fragment to store history.
1110 | } else if (this._wantsHashChange) {
1111 | this.fragment = frag;
1112 | this._updateHash(window.location, frag, options.replace);
1113 | if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
1114 | // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
1115 | // When replace is true, we don't want this.
1116 | if(!options.replace) this.iframe.document.open().close();
1117 | this._updateHash(this.iframe.location, frag, options.replace);
1118 | }
1119 |
1120 | // If you've told us that you explicitly don't want fallback hashchange-
1121 | // based history, then `navigate` becomes a page refresh.
1122 | } else {
1123 | window.location.assign(this.options.root + fragment);
1124 | }
1125 | if (options.trigger) this.loadUrl(fragment);
1126 | },
1127 |
1128 | // Update the hash location, either replacing the current entry, or adding
1129 | // a new one to the browser history.
1130 | _updateHash: function(location, fragment, replace) {
1131 | if (replace) {
1132 | location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
1133 | } else {
1134 | location.hash = fragment;
1135 | }
1136 | }
1137 | });
1138 |
1139 | // Backbone.View
1140 | // -------------
1141 |
1142 | // Creating a Backbone.View creates its initial element outside of the DOM,
1143 | // if an existing element is not provided...
1144 | var View = Backbone.View = function(options) {
1145 | this.cid = _.uniqueId('view');
1146 | this._configure(options || {});
1147 | this._ensureElement();
1148 | this.initialize.apply(this, arguments);
1149 | this.delegateEvents();
1150 | };
1151 |
1152 | // Cached regex to split keys for `delegate`.
1153 | var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1154 |
1155 | // List of view options to be merged as properties.
1156 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
1157 |
1158 | // Set up all inheritable **Backbone.View** properties and methods.
1159 | _.extend(View.prototype, Events, {
1160 |
1161 | // The default `tagName` of a View's element is `"div"`.
1162 | tagName: 'div',
1163 |
1164 | // jQuery delegate for element lookup, scoped to DOM elements within the
1165 | // current view. This should be prefered to global lookups where possible.
1166 | $: function(selector) {
1167 | return this.$el.find(selector);
1168 | },
1169 |
1170 | // Initialize is an empty function by default. Override it with your own
1171 | // initialization logic.
1172 | initialize: function(){},
1173 |
1174 | // **render** is the core function that your view should override, in order
1175 | // to populate its element (`this.el`), with the appropriate HTML. The
1176 | // convention is for **render** to always return `this`.
1177 | render: function() {
1178 | return this;
1179 | },
1180 |
1181 | // Remove this view from the DOM. Note that the view isn't present in the
1182 | // DOM by default, so calling this method may be a no-op.
1183 | remove: function() {
1184 | this.$el.remove();
1185 | return this;
1186 | },
1187 |
1188 | // For small amounts of DOM Elements, where a full-blown template isn't
1189 | // needed, use **make** to manufacture elements, one at a time.
1190 | //
1191 | // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
1192 | //
1193 | make: function(tagName, attributes, content) {
1194 | var el = document.createElement(tagName);
1195 | if (attributes) $(el).attr(attributes);
1196 | if (content != null) $(el).html(content);
1197 | return el;
1198 | },
1199 |
1200 | // Change the view's element (`this.el` property), including event
1201 | // re-delegation.
1202 | setElement: function(element, delegate) {
1203 | if (this.$el) this.undelegateEvents();
1204 | this.$el = (element instanceof $) ? element : $(element);
1205 | this.el = this.$el[0];
1206 | if (delegate !== false) this.delegateEvents();
1207 | return this;
1208 | },
1209 |
1210 | // Set callbacks, where `this.events` is a hash of
1211 | //
1212 | // *{"event selector": "callback"}*
1213 | //
1214 | // {
1215 | // 'mousedown .title': 'edit',
1216 | // 'click .button': 'save'
1217 | // 'click .open': function(e) { ... }
1218 | // }
1219 | //
1220 | // pairs. Callbacks will be bound to the view, with `this` set properly.
1221 | // Uses event delegation for efficiency.
1222 | // Omitting the selector binds the event to `this.el`.
1223 | // This only works for delegate-able events: not `focus`, `blur`, and
1224 | // not `change`, `submit`, and `reset` in Internet Explorer.
1225 | delegateEvents: function(events) {
1226 | if (!(events || (events = getValue(this, 'events')))) return;
1227 | this.undelegateEvents();
1228 | for (var key in events) {
1229 | var method = events[key];
1230 | if (!_.isFunction(method)) method = this[events[key]];
1231 | if (!method) throw new Error('Method "' + events[key] + '" does not exist');
1232 | var match = key.match(delegateEventSplitter);
1233 | var eventName = match[1], selector = match[2];
1234 | method = _.bind(method, this);
1235 | eventName += '.delegateEvents' + this.cid;
1236 | if (selector === '') {
1237 | this.$el.bind(eventName, method);
1238 | } else {
1239 | this.$el.delegate(selector, eventName, method);
1240 | }
1241 | }
1242 | },
1243 |
1244 | // Clears all callbacks previously bound to the view with `delegateEvents`.
1245 | // You usually don't need to use this, but may wish to if you have multiple
1246 | // Backbone views attached to the same DOM element.
1247 | undelegateEvents: function() {
1248 | this.$el.unbind('.delegateEvents' + this.cid);
1249 | },
1250 |
1251 | // Performs the initial configuration of a View with a set of options.
1252 | // Keys with special meaning *(model, collection, id, className)*, are
1253 | // attached directly to the view.
1254 | _configure: function(options) {
1255 | if (this.options) options = _.extend({}, this.options, options);
1256 | for (var i = 0, l = viewOptions.length; i < l; i++) {
1257 | var attr = viewOptions[i];
1258 | if (options[attr]) this[attr] = options[attr];
1259 | }
1260 | this.options = options;
1261 | },
1262 |
1263 | // Ensure that the View has a DOM element to render into.
1264 | // If `this.el` is a string, pass it through `$()`, take the first
1265 | // matching element, and re-assign it to `el`. Otherwise, create
1266 | // an element from the `id`, `className` and `tagName` properties.
1267 | _ensureElement: function() {
1268 | if (!this.el) {
1269 | var attrs = getValue(this, 'attributes') || {};
1270 | if (this.id) attrs.id = this.id;
1271 | if (this.className) attrs['class'] = this.className;
1272 | this.setElement(this.make(this.tagName, attrs), false);
1273 | } else {
1274 | this.setElement(this.el, false);
1275 | }
1276 | }
1277 |
1278 | });
1279 |
1280 | // The self-propagating extend function that Backbone classes use.
1281 | var extend = function (protoProps, classProps) {
1282 | var child = inherits(this, protoProps, classProps);
1283 | child.extend = this.extend;
1284 | return child;
1285 | };
1286 |
1287 | // Set up inheritance for the model, collection, and view.
1288 | Model.extend = Collection.extend = Router.extend = View.extend = extend;
1289 |
1290 | // Backbone.sync
1291 | // -------------
1292 |
1293 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1294 | var methodMap = {
1295 | 'create': 'POST',
1296 | 'update': 'PUT',
1297 | 'delete': 'DELETE',
1298 | 'read': 'GET'
1299 | };
1300 |
1301 | // Override this function to change the manner in which Backbone persists
1302 | // models to the server. You will be passed the type of request, and the
1303 | // model in question. By default, makes a RESTful Ajax request
1304 | // to the model's `url()`. Some possible customizations could be:
1305 | //
1306 | // * Use `setTimeout` to batch rapid-fire updates into a single request.
1307 | // * Send up the models as XML instead of JSON.
1308 | // * Persist models via WebSockets instead of Ajax.
1309 | //
1310 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1311 | // as `POST`, with a `_method` parameter containing the true HTTP method,
1312 | // as well as all requests with the body as `application/x-www-form-urlencoded`
1313 | // instead of `application/json` with the model in a param named `model`.
1314 | // Useful when interfacing with server-side languages like **PHP** that make
1315 | // it difficult to read the body of `PUT` requests.
1316 | Backbone.sync = function(method, model, options) {
1317 | var type = methodMap[method];
1318 |
1319 | // Default options, unless specified.
1320 | options || (options = {});
1321 |
1322 | // Default JSON-request options.
1323 | var params = {type: type, dataType: 'json'};
1324 |
1325 | // Ensure that we have a URL.
1326 | if (!options.url) {
1327 | params.url = getValue(model, 'url') || urlError();
1328 | }
1329 |
1330 | // Ensure that we have the appropriate request data.
1331 | if (!options.data && model && (method == 'create' || method == 'update')) {
1332 | params.contentType = 'application/json';
1333 | params.data = JSON.stringify(model.toJSON());
1334 | }
1335 |
1336 | // For older servers, emulate JSON by encoding the request into an HTML-form.
1337 | if (Backbone.emulateJSON) {
1338 | params.contentType = 'application/x-www-form-urlencoded';
1339 | params.data = params.data ? {model: params.data} : {};
1340 | }
1341 |
1342 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1343 | // And an `X-HTTP-Method-Override` header.
1344 | if (Backbone.emulateHTTP) {
1345 | if (type === 'PUT' || type === 'DELETE') {
1346 | if (Backbone.emulateJSON) params.data._method = type;
1347 | params.type = 'POST';
1348 | params.beforeSend = function(xhr) {
1349 | xhr.setRequestHeader('X-HTTP-Method-Override', type);
1350 | };
1351 | }
1352 | }
1353 |
1354 | // Don't process data on a non-GET request.
1355 | if (params.type !== 'GET' && !Backbone.emulateJSON) {
1356 | params.processData = false;
1357 | }
1358 |
1359 | // Make the request, allowing the user to override any Ajax options.
1360 | return $.ajax(_.extend(params, options));
1361 | };
1362 |
1363 | // Wrap an optional error callback with a fallback error event.
1364 | Backbone.wrapError = function(onError, originalModel, options) {
1365 | return function(model, resp) {
1366 | resp = model === originalModel ? resp : model;
1367 | if (onError) {
1368 | onError(originalModel, resp, options);
1369 | } else {
1370 | originalModel.trigger('error', originalModel, resp, options);
1371 | }
1372 | };
1373 | };
1374 |
1375 | // Helpers
1376 | // -------
1377 |
1378 | // Shared empty constructor function to aid in prototype-chain creation.
1379 | var ctor = function(){};
1380 |
1381 | // Helper function to correctly set up the prototype chain, for subclasses.
1382 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
1383 | // class properties to be extended.
1384 | var inherits = function(parent, protoProps, staticProps) {
1385 | var child;
1386 |
1387 | // The constructor function for the new subclass is either defined by you
1388 | // (the "constructor" property in your `extend` definition), or defaulted
1389 | // by us to simply call the parent's constructor.
1390 | if (protoProps && protoProps.hasOwnProperty('constructor')) {
1391 | child = protoProps.constructor;
1392 | } else {
1393 | child = function(){ parent.apply(this, arguments); };
1394 | }
1395 |
1396 | // Inherit class (static) properties from parent.
1397 | _.extend(child, parent);
1398 |
1399 | // Set the prototype chain to inherit from `parent`, without calling
1400 | // `parent`'s constructor function.
1401 | ctor.prototype = parent.prototype;
1402 | child.prototype = new ctor();
1403 |
1404 | // Add prototype properties (instance properties) to the subclass,
1405 | // if supplied.
1406 | if (protoProps) _.extend(child.prototype, protoProps);
1407 |
1408 | // Add static properties to the constructor function, if supplied.
1409 | if (staticProps) _.extend(child, staticProps);
1410 |
1411 | // Correctly set child's `prototype.constructor`.
1412 | child.prototype.constructor = child;
1413 |
1414 | // Set a convenience property in case the parent's prototype is needed later.
1415 | child.__super__ = parent.prototype;
1416 |
1417 | return child;
1418 | };
1419 |
1420 | // Helper function to get a value from a Backbone object as a property
1421 | // or as a function.
1422 | var getValue = function(object, prop) {
1423 | if (!(object && object[prop])) return null;
1424 | return _.isFunction(object[prop]) ? object[prop]() : object[prop];
1425 | };
1426 |
1427 | // Throw an error when a URL is needed, and none is supplied.
1428 | var urlError = function() {
1429 | throw new Error('A "url" property or function must be specified');
1430 | };
1431 |
1432 | return Backbone;
1433 | };
1434 |
1435 | // Export for browserify
1436 | if (module == null) { module = {}; };
1437 | module.exports = create(this);
1438 |
1439 | }(this);
--------------------------------------------------------------------------------