backbone-rails 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Alexander Flatter
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # backbone-rails
2
+
3
+ Developing javascript applications on top of rails just got faster thanks to the Rails 3.1 asset pipeline.
4
+ Like [jquery-rails](https://github.com/indirect/jquery-rails/), this gem bundles some javascript files to make them available to your application:
5
+
6
+ * [backbone](http://documentcloud.github.com/backbone)
7
+ * [underscore](http://documentcloud.github.com/underscore)
8
+ * [json2](https://github.com/douglascrockford/JSON-js)
9
+
10
+ ## How to use it
11
+
12
+ Add it to your Gemfile:
13
+
14
+ gem 'backbone-rails'
15
+
16
+ Require what you need in `app/assets/javascripts/application.js.coffee`:
17
+
18
+ #= require json2
19
+ #= require underscore
20
+ #= require backbone
21
+
22
+ With pure javascript, the lines would look like `app/assets/javascripts/application.js`:
23
+
24
+ //= require json2
25
+ //= require underscore
26
+ //= require backbone
27
+
28
+ ## Versioning
29
+
30
+ The gem will follow backbone versioning.
@@ -0,0 +1,9 @@
1
+
2
+ module Backbone
3
+ module Rails
4
+
5
+ class Engine < ::Rails::Engine
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,1011 @@
1
+ // Backbone.js 0.3.3
2
+ // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
3
+ // Backbone may be freely distributed under the MIT license.
4
+ // For all details and documentation:
5
+ // http://documentcloud.github.com/backbone
6
+
7
+ (function(){
8
+
9
+ // Initial Setup
10
+ // -------------
11
+
12
+ // The top-level namespace. All public Backbone classes and modules will
13
+ // be attached to this. Exported for both CommonJS and the browser.
14
+ var Backbone;
15
+ if (typeof exports !== 'undefined') {
16
+ Backbone = exports;
17
+ } else {
18
+ Backbone = this.Backbone = {};
19
+ }
20
+
21
+ // Current version of the library. Keep in sync with `package.json`.
22
+ Backbone.VERSION = '0.3.3';
23
+
24
+ // Require Underscore, if we're on the server, and it's not already present.
25
+ var _ = this._;
26
+ if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._;
27
+
28
+ // For Backbone's purposes, either jQuery or Zepto owns the `$` variable.
29
+ var $ = this.jQuery || this.Zepto;
30
+
31
+ // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will
32
+ // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
33
+ // `X-Http-Method-Override` header.
34
+ Backbone.emulateHTTP = false;
35
+
36
+ // Turn on `emulateJSON` to support legacy servers that can't deal with direct
37
+ // `application/json` requests ... will encode the body as
38
+ // `application/x-www-form-urlencoded` instead and will send the model in a
39
+ // form param named `model`.
40
+ Backbone.emulateJSON = false;
41
+
42
+ // Backbone.Events
43
+ // -----------------
44
+
45
+ // A module that can be mixed in to *any object* in order to provide it with
46
+ // custom events. You may `bind` or `unbind` a callback function to an event;
47
+ // `trigger`-ing an event fires all callbacks in succession.
48
+ //
49
+ // var object = {};
50
+ // _.extend(object, Backbone.Events);
51
+ // object.bind('expand', function(){ alert('expanded'); });
52
+ // object.trigger('expand');
53
+ //
54
+ Backbone.Events = {
55
+
56
+ // Bind an event, specified by a string name, `ev`, to a `callback` function.
57
+ // Passing `"all"` will bind the callback to all events fired.
58
+ bind : function(ev, callback) {
59
+ var calls = this._callbacks || (this._callbacks = {});
60
+ var list = this._callbacks[ev] || (this._callbacks[ev] = []);
61
+ list.push(callback);
62
+ return this;
63
+ },
64
+
65
+ // Remove one or many callbacks. If `callback` is null, removes all
66
+ // callbacks for the event. If `ev` is null, removes all bound callbacks
67
+ // for all events.
68
+ unbind : function(ev, callback) {
69
+ var calls;
70
+ if (!ev) {
71
+ this._callbacks = {};
72
+ } else if (calls = this._callbacks) {
73
+ if (!callback) {
74
+ calls[ev] = [];
75
+ } else {
76
+ var list = calls[ev];
77
+ if (!list) return this;
78
+ for (var i = 0, l = list.length; i < l; i++) {
79
+ if (callback === list[i]) {
80
+ list.splice(i, 1);
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return this;
87
+ },
88
+
89
+ // Trigger an event, firing all bound callbacks. Callbacks are passed the
90
+ // same arguments as `trigger` is, apart from the event name.
91
+ // Listening for `"all"` passes the true event name as the first argument.
92
+ trigger : function(ev) {
93
+ var list, calls, i, l;
94
+ if (!(calls = this._callbacks)) return this;
95
+ if (list = calls[ev]) {
96
+ for (i = 0, l = list.length; i < l; i++) {
97
+ list[i].apply(this, Array.prototype.slice.call(arguments, 1));
98
+ }
99
+ }
100
+ if (list = calls['all']) {
101
+ for (i = 0, l = list.length; i < l; i++) {
102
+ list[i].apply(this, arguments);
103
+ }
104
+ }
105
+ return this;
106
+ }
107
+
108
+ };
109
+
110
+ // Backbone.Model
111
+ // --------------
112
+
113
+ // Create a new model, with defined attributes. A client id (`cid`)
114
+ // is automatically generated and assigned for you.
115
+ Backbone.Model = function(attributes, options) {
116
+ attributes || (attributes = {});
117
+ if (this.defaults) attributes = _.extend({}, this.defaults, attributes);
118
+ this.attributes = {};
119
+ this._escapedAttributes = {};
120
+ this.cid = _.uniqueId('c');
121
+ this.set(attributes, {silent : true});
122
+ this._previousAttributes = _.clone(this.attributes);
123
+ if (options && options.collection) this.collection = options.collection;
124
+ this.initialize(attributes, options);
125
+ };
126
+
127
+ // Attach all inheritable methods to the Model prototype.
128
+ _.extend(Backbone.Model.prototype, Backbone.Events, {
129
+
130
+ // A snapshot of the model's previous attributes, taken immediately
131
+ // after the last `"change"` event was fired.
132
+ _previousAttributes : null,
133
+
134
+ // Has the item been changed since the last `"change"` event?
135
+ _changed : false,
136
+
137
+ // Initialize is an empty function by default. Override it with your own
138
+ // initialization logic.
139
+ initialize : function(){},
140
+
141
+ // Return a copy of the model's `attributes` object.
142
+ toJSON : function() {
143
+ return _.clone(this.attributes);
144
+ },
145
+
146
+ // Get the value of an attribute.
147
+ get : function(attr) {
148
+ return this.attributes[attr];
149
+ },
150
+
151
+ // Get the HTML-escaped value of an attribute.
152
+ escape : function(attr) {
153
+ var html;
154
+ if (html = this._escapedAttributes[attr]) return html;
155
+ var val = this.attributes[attr];
156
+ return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val);
157
+ },
158
+
159
+ // Set a hash of model attributes on the object, firing `"change"` unless you
160
+ // choose to silence it.
161
+ set : function(attrs, options) {
162
+
163
+ // Extract attributes and options.
164
+ options || (options = {});
165
+ if (!attrs) return this;
166
+ if (attrs.attributes) attrs = attrs.attributes;
167
+ var now = this.attributes, escaped = this._escapedAttributes;
168
+
169
+ // Run validation.
170
+ if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
171
+
172
+ // Check for changes of `id`.
173
+ if ('id' in attrs) this.id = attrs.id;
174
+
175
+ // Update attributes.
176
+ for (var attr in attrs) {
177
+ var val = attrs[attr];
178
+ if (!_.isEqual(now[attr], val)) {
179
+ now[attr] = val;
180
+ delete escaped[attr];
181
+ if (!options.silent) {
182
+ this._changed = true;
183
+ this.trigger('change:' + attr, this, val, options);
184
+ }
185
+ }
186
+ }
187
+
188
+ // Fire the `"change"` event, if the model has been changed.
189
+ if (!options.silent && this._changed) this.change(options);
190
+ return this;
191
+ },
192
+
193
+ // Remove an attribute from the model, firing `"change"` unless you choose
194
+ // to silence it.
195
+ unset : function(attr, options) {
196
+ options || (options = {});
197
+ var value = this.attributes[attr];
198
+
199
+ // Run validation.
200
+ var validObj = {};
201
+ validObj[attr] = void 0;
202
+ if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
203
+
204
+ // Remove the attribute.
205
+ delete this.attributes[attr];
206
+ delete this._escapedAttributes[attr];
207
+ if (!options.silent) {
208
+ this._changed = true;
209
+ this.trigger('change:' + attr, this, void 0, options);
210
+ this.change(options);
211
+ }
212
+ return this;
213
+ },
214
+
215
+ // Clear all attributes on the model, firing `"change"` unless you choose
216
+ // to silence it.
217
+ clear : function(options) {
218
+ options || (options = {});
219
+ var old = this.attributes;
220
+
221
+ // Run validation.
222
+ var validObj = {};
223
+ for (attr in old) validObj[attr] = void 0;
224
+ if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
225
+
226
+ this.attributes = {};
227
+ this._escapedAttributes = {};
228
+ if (!options.silent) {
229
+ this._changed = true;
230
+ for (attr in old) {
231
+ this.trigger('change:' + attr, this, void 0, options);
232
+ }
233
+ this.change(options);
234
+ }
235
+ return this;
236
+ },
237
+
238
+ // Fetch the model from the server. If the server's representation of the
239
+ // model differs from its current attributes, they will be overriden,
240
+ // triggering a `"change"` event.
241
+ fetch : function(options) {
242
+ options || (options = {});
243
+ var model = this;
244
+ var success = function(resp) {
245
+ if (!model.set(model.parse(resp), options)) return false;
246
+ if (options.success) options.success(model, resp);
247
+ };
248
+ var error = wrapError(options.error, model, options);
249
+ (this.sync || Backbone.sync)('read', this, success, error);
250
+ return this;
251
+ },
252
+
253
+ // Set a hash of model attributes, and sync the model to the server.
254
+ // If the server returns an attributes hash that differs, the model's
255
+ // state will be `set` again.
256
+ save : function(attrs, options) {
257
+ options || (options = {});
258
+ if (attrs && !this.set(attrs, options)) return false;
259
+ var model = this;
260
+ var success = function(resp) {
261
+ if (!model.set(model.parse(resp), options)) return false;
262
+ if (options.success) options.success(model, resp);
263
+ };
264
+ var error = wrapError(options.error, model, options);
265
+ var method = this.isNew() ? 'create' : 'update';
266
+ (this.sync || Backbone.sync)(method, this, success, error);
267
+ return this;
268
+ },
269
+
270
+ // Destroy this model on the server. Upon success, the model is removed
271
+ // from its collection, if it has one.
272
+ destroy : function(options) {
273
+ options || (options = {});
274
+ var model = this;
275
+ var success = function(resp) {
276
+ if (model.collection) model.collection.remove(model);
277
+ if (options.success) options.success(model, resp);
278
+ };
279
+ var error = wrapError(options.error, model, options);
280
+ (this.sync || Backbone.sync)('delete', this, success, error);
281
+ return this;
282
+ },
283
+
284
+ // Default URL for the model's representation on the server -- if you're
285
+ // using Backbone's restful methods, override this to change the endpoint
286
+ // that will be called.
287
+ url : function() {
288
+ var base = getUrl(this.collection);
289
+ if (this.isNew()) return base;
290
+ return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id;
291
+ },
292
+
293
+ // **parse** converts a response into the hash of attributes to be `set` on
294
+ // the model. The default implementation is just to pass the response along.
295
+ parse : function(resp) {
296
+ return resp;
297
+ },
298
+
299
+ // Create a new model with identical attributes to this one.
300
+ clone : function() {
301
+ return new this.constructor(this);
302
+ },
303
+
304
+ // A model is new if it has never been saved to the server, and has a negative
305
+ // ID.
306
+ isNew : function() {
307
+ return !this.id;
308
+ },
309
+
310
+ // Call this method to manually fire a `change` event for this model.
311
+ // Calling this will cause all objects observing the model to update.
312
+ change : function(options) {
313
+ this.trigger('change', this, options);
314
+ this._previousAttributes = _.clone(this.attributes);
315
+ this._changed = false;
316
+ },
317
+
318
+ // Determine if the model has changed since the last `"change"` event.
319
+ // If you specify an attribute name, determine if that attribute has changed.
320
+ hasChanged : function(attr) {
321
+ if (attr) return this._previousAttributes[attr] != this.attributes[attr];
322
+ return this._changed;
323
+ },
324
+
325
+ // Return an object containing all the attributes that have changed, or false
326
+ // if there are no changed attributes. Useful for determining what parts of a
327
+ // view need to be updated and/or what attributes need to be persisted to
328
+ // the server.
329
+ changedAttributes : function(now) {
330
+ now || (now = this.attributes);
331
+ var old = this._previousAttributes;
332
+ var changed = false;
333
+ for (var attr in now) {
334
+ if (!_.isEqual(old[attr], now[attr])) {
335
+ changed = changed || {};
336
+ changed[attr] = now[attr];
337
+ }
338
+ }
339
+ return changed;
340
+ },
341
+
342
+ // Get the previous value of an attribute, recorded at the time the last
343
+ // `"change"` event was fired.
344
+ previous : function(attr) {
345
+ if (!attr || !this._previousAttributes) return null;
346
+ return this._previousAttributes[attr];
347
+ },
348
+
349
+ // Get all of the attributes of the model at the time of the previous
350
+ // `"change"` event.
351
+ previousAttributes : function() {
352
+ return _.clone(this._previousAttributes);
353
+ },
354
+
355
+ // Run validation against a set of incoming attributes, returning `true`
356
+ // if all is well. If a specific `error` callback has been passed,
357
+ // call that instead of firing the general `"error"` event.
358
+ _performValidation : function(attrs, options) {
359
+ var error = this.validate(attrs);
360
+ if (error) {
361
+ if (options.error) {
362
+ options.error(this, error);
363
+ } else {
364
+ this.trigger('error', this, error, options);
365
+ }
366
+ return false;
367
+ }
368
+ return true;
369
+ }
370
+
371
+ });
372
+
373
+ // Backbone.Collection
374
+ // -------------------
375
+
376
+ // Provides a standard collection class for our sets of models, ordered
377
+ // or unordered. If a `comparator` is specified, the Collection will maintain
378
+ // its models in sort order, as they're added and removed.
379
+ Backbone.Collection = function(models, options) {
380
+ options || (options = {});
381
+ if (options.comparator) {
382
+ this.comparator = options.comparator;
383
+ delete options.comparator;
384
+ }
385
+ this._boundOnModelEvent = _.bind(this._onModelEvent, this);
386
+ this._reset();
387
+ if (models) this.refresh(models, {silent: true});
388
+ this.initialize(models, options);
389
+ };
390
+
391
+ // Define the Collection's inheritable methods.
392
+ _.extend(Backbone.Collection.prototype, Backbone.Events, {
393
+
394
+ // The default model for a collection is just a **Backbone.Model**.
395
+ // This should be overridden in most cases.
396
+ model : Backbone.Model,
397
+
398
+ // Initialize is an empty function by default. Override it with your own
399
+ // initialization logic.
400
+ initialize : function(){},
401
+
402
+ // The JSON representation of a Collection is an array of the
403
+ // models' attributes.
404
+ toJSON : function() {
405
+ return this.map(function(model){ return model.toJSON(); });
406
+ },
407
+
408
+ // Add a model, or list of models to the set. Pass **silent** to avoid
409
+ // firing the `added` event for every new model.
410
+ add : function(models, options) {
411
+ if (_.isArray(models)) {
412
+ for (var i = 0, l = models.length; i < l; i++) {
413
+ this._add(models[i], options);
414
+ }
415
+ } else {
416
+ this._add(models, options);
417
+ }
418
+ return this;
419
+ },
420
+
421
+ // Remove a model, or a list of models from the set. Pass silent to avoid
422
+ // firing the `removed` event for every model removed.
423
+ remove : function(models, options) {
424
+ if (_.isArray(models)) {
425
+ for (var i = 0, l = models.length; i < l; i++) {
426
+ this._remove(models[i], options);
427
+ }
428
+ } else {
429
+ this._remove(models, options);
430
+ }
431
+ return this;
432
+ },
433
+
434
+ // Get a model from the set by id.
435
+ get : function(id) {
436
+ if (id == null) return null;
437
+ return this._byId[id.id != null ? id.id : id];
438
+ },
439
+
440
+ // Get a model from the set by client id.
441
+ getByCid : function(cid) {
442
+ return cid && this._byCid[cid.cid || cid];
443
+ },
444
+
445
+ // Get the model at the given index.
446
+ at: function(index) {
447
+ return this.models[index];
448
+ },
449
+
450
+ // Force the collection to re-sort itself. You don't need to call this under normal
451
+ // circumstances, as the set will maintain sort order as each item is added.
452
+ sort : function(options) {
453
+ options || (options = {});
454
+ if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
455
+ this.models = this.sortBy(this.comparator);
456
+ if (!options.silent) this.trigger('refresh', this, options);
457
+ return this;
458
+ },
459
+
460
+ // Pluck an attribute from each model in the collection.
461
+ pluck : function(attr) {
462
+ return _.map(this.models, function(model){ return model.get(attr); });
463
+ },
464
+
465
+ // When you have more items than you want to add or remove individually,
466
+ // you can refresh the entire set with a new list of models, without firing
467
+ // any `added` or `removed` events. Fires `refresh` when finished.
468
+ refresh : function(models, options) {
469
+ models || (models = []);
470
+ options || (options = {});
471
+ this._reset();
472
+ this.add(models, {silent: true});
473
+ if (!options.silent) this.trigger('refresh', this, options);
474
+ return this;
475
+ },
476
+
477
+ // Fetch the default set of models for this collection, refreshing the
478
+ // collection when they arrive.
479
+ fetch : function(options) {
480
+ options || (options = {});
481
+ var collection = this;
482
+ var success = function(resp) {
483
+ collection.refresh(collection.parse(resp));
484
+ if (options.success) options.success(collection, resp);
485
+ };
486
+ var error = wrapError(options.error, collection, options);
487
+ (this.sync || Backbone.sync)('read', this, success, error);
488
+ return this;
489
+ },
490
+
491
+ // Create a new instance of a model in this collection. After the model
492
+ // has been created on the server, it will be added to the collection.
493
+ create : function(model, options) {
494
+ var coll = this;
495
+ options || (options = {});
496
+ if (!(model instanceof Backbone.Model)) {
497
+ model = new this.model(model, {collection: coll});
498
+ } else {
499
+ model.collection = coll;
500
+ }
501
+ var success = function(nextModel, resp) {
502
+ coll.add(nextModel);
503
+ if (options.success) options.success(nextModel, resp);
504
+ };
505
+ return model.save(null, {success : success, error : options.error});
506
+ },
507
+
508
+ // **parse** converts a response into a list of models to be added to the
509
+ // collection. The default implementation is just to pass it through.
510
+ parse : function(resp) {
511
+ return resp;
512
+ },
513
+
514
+ // Proxy to _'s chain. Can't be proxied the same way the rest of the
515
+ // underscore methods are proxied because it relies on the underscore
516
+ // constructor.
517
+ chain: function () {
518
+ return _(this.models).chain();
519
+ },
520
+
521
+ // Reset all internal state. Called when the collection is refreshed.
522
+ _reset : function(options) {
523
+ this.length = 0;
524
+ this.models = [];
525
+ this._byId = {};
526
+ this._byCid = {};
527
+ },
528
+
529
+ // Internal implementation of adding a single model to the set, updating
530
+ // hash indexes for `id` and `cid` lookups.
531
+ _add : function(model, options) {
532
+ options || (options = {});
533
+ if (!(model instanceof Backbone.Model)) {
534
+ model = new this.model(model, {collection: this});
535
+ }
536
+ var already = this.getByCid(model);
537
+ if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
538
+ this._byId[model.id] = model;
539
+ this._byCid[model.cid] = model;
540
+ model.collection = this;
541
+ var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length;
542
+ this.models.splice(index, 0, model);
543
+ model.bind('all', this._boundOnModelEvent);
544
+ this.length++;
545
+ if (!options.silent) model.trigger('add', model, this, options);
546
+ return model;
547
+ },
548
+
549
+ // Internal implementation of removing a single model from the set, updating
550
+ // hash indexes for `id` and `cid` lookups.
551
+ _remove : function(model, options) {
552
+ options || (options = {});
553
+ model = this.getByCid(model) || this.get(model);
554
+ if (!model) return null;
555
+ delete this._byId[model.id];
556
+ delete this._byCid[model.cid];
557
+ delete model.collection;
558
+ this.models.splice(this.indexOf(model), 1);
559
+ this.length--;
560
+ if (!options.silent) model.trigger('remove', model, this, options);
561
+ model.unbind('all', this._boundOnModelEvent);
562
+ return model;
563
+ },
564
+
565
+ // Internal method called every time a model in the set fires an event.
566
+ // Sets need to update their indexes when models change ids. All other
567
+ // events simply proxy through.
568
+ _onModelEvent : function(ev, model) {
569
+ if (ev === 'change:id') {
570
+ delete this._byId[model.previous('id')];
571
+ this._byId[model.id] = model;
572
+ }
573
+ this.trigger.apply(this, arguments);
574
+ }
575
+
576
+ });
577
+
578
+ // Underscore methods that we want to implement on the Collection.
579
+ var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
580
+ 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
581
+ 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
582
+ 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty'];
583
+
584
+ // Mix in each Underscore method as a proxy to `Collection#models`.
585
+ _.each(methods, function(method) {
586
+ Backbone.Collection.prototype[method] = function() {
587
+ return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
588
+ };
589
+ });
590
+
591
+ // Backbone.Controller
592
+ // -------------------
593
+
594
+ // Controllers map faux-URLs to actions, and fire events when routes are
595
+ // matched. Creating a new one sets its `routes` hash, if not set statically.
596
+ Backbone.Controller = function(options) {
597
+ options || (options = {});
598
+ if (options.routes) this.routes = options.routes;
599
+ this._bindRoutes();
600
+ this.initialize(options);
601
+ };
602
+
603
+ // Cached regular expressions for matching named param parts and splatted
604
+ // parts of route strings.
605
+ var namedParam = /:([\w\d]+)/g;
606
+ var splatParam = /\*([\w\d]+)/g;
607
+
608
+ // Set up all inheritable **Backbone.Controller** properties and methods.
609
+ _.extend(Backbone.Controller.prototype, Backbone.Events, {
610
+
611
+ // Initialize is an empty function by default. Override it with your own
612
+ // initialization logic.
613
+ initialize : function(){},
614
+
615
+ // Manually bind a single named route to a callback. For example:
616
+ //
617
+ // this.route('search/:query/p:num', 'search', function(query, num) {
618
+ // ...
619
+ // });
620
+ //
621
+ route : function(route, name, callback) {
622
+ Backbone.history || (Backbone.history = new Backbone.History);
623
+ if (!_.isRegExp(route)) route = this._routeToRegExp(route);
624
+ Backbone.history.route(route, _.bind(function(fragment) {
625
+ var args = this._extractParameters(route, fragment);
626
+ callback.apply(this, args);
627
+ this.trigger.apply(this, ['route:' + name].concat(args));
628
+ }, this));
629
+ },
630
+
631
+ // Simple proxy to `Backbone.history` to save a fragment into the history,
632
+ // without triggering routes.
633
+ saveLocation : function(fragment) {
634
+ Backbone.history.saveLocation(fragment);
635
+ },
636
+
637
+ // Bind all defined routes to `Backbone.history`.
638
+ _bindRoutes : function() {
639
+ if (!this.routes) return;
640
+ for (var route in this.routes) {
641
+ var name = this.routes[route];
642
+ this.route(route, name, this[name]);
643
+ }
644
+ },
645
+
646
+ // Convert a route string into a regular expression, suitable for matching
647
+ // against the current location fragment.
648
+ _routeToRegExp : function(route) {
649
+ route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)");
650
+ return new RegExp('^' + route + '$');
651
+ },
652
+
653
+ // Given a route, and a URL fragment that it matches, return the array of
654
+ // extracted parameters.
655
+ _extractParameters : function(route, fragment) {
656
+ return route.exec(fragment).slice(1);
657
+ }
658
+
659
+ });
660
+
661
+ // Backbone.History
662
+ // ----------------
663
+
664
+ // Handles cross-browser history management, based on URL hashes. If the
665
+ // browser does not support `onhashchange`, falls back to polling.
666
+ Backbone.History = function() {
667
+ this.handlers = [];
668
+ this.fragment = this.getFragment();
669
+ _.bindAll(this, 'checkUrl');
670
+ };
671
+
672
+ // Cached regex for cleaning hashes.
673
+ var hashStrip = /^#*/;
674
+
675
+ // Set up all inheritable **Backbone.History** properties and methods.
676
+ _.extend(Backbone.History.prototype, {
677
+
678
+ // The default interval to poll for hash changes, if necessary, is
679
+ // twenty times a second.
680
+ interval: 50,
681
+
682
+ // Get the cross-browser normalized URL fragment.
683
+ getFragment : function(loc) {
684
+ return (loc || window.location).hash.replace(hashStrip, '');
685
+ },
686
+
687
+ // Start the hash change handling, returning `true` if the current URL matches
688
+ // an existing route, and `false` otherwise.
689
+ start : function() {
690
+ var docMode = document.documentMode;
691
+ var oldIE = ($.browser.msie && (!docMode || docMode <= 7));
692
+ if (oldIE) {
693
+ this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
694
+ }
695
+ if ('onhashchange' in window && !oldIE) {
696
+ $(window).bind('hashchange', this.checkUrl);
697
+ } else {
698
+ setInterval(this.checkUrl, this.interval);
699
+ }
700
+ return this.loadUrl();
701
+ },
702
+
703
+ // Add a route to be tested when the hash changes. Routes are matched in the
704
+ // order they are added.
705
+ route : function(route, callback) {
706
+ this.handlers.push({route : route, callback : callback});
707
+ },
708
+
709
+ // Checks the current URL to see if it has changed, and if it has,
710
+ // calls `loadUrl`, normalizing across the hidden iframe.
711
+ checkUrl : function() {
712
+ var current = this.getFragment();
713
+ if (current == this.fragment && this.iframe) {
714
+ current = this.getFragment(this.iframe.location);
715
+ }
716
+ if (current == this.fragment ||
717
+ current == decodeURIComponent(this.fragment)) return false;
718
+ if (this.iframe) {
719
+ window.location.hash = this.iframe.location.hash = current;
720
+ }
721
+ this.loadUrl();
722
+ },
723
+
724
+ // Attempt to load the current URL fragment. If a route succeeds with a
725
+ // match, returns `true`. If no defined routes matches the fragment,
726
+ // returns `false`.
727
+ loadUrl : function() {
728
+ var fragment = this.fragment = this.getFragment();
729
+ var matched = _.any(this.handlers, function(handler) {
730
+ if (handler.route.test(fragment)) {
731
+ handler.callback(fragment);
732
+ return true;
733
+ }
734
+ });
735
+ return matched;
736
+ },
737
+
738
+ // Save a fragment into the hash history. You are responsible for properly
739
+ // URL-encoding the fragment in advance. This does not trigger
740
+ // a `hashchange` event.
741
+ saveLocation : function(fragment) {
742
+ fragment = (fragment || '').replace(hashStrip, '');
743
+ if (this.fragment == fragment) return;
744
+ window.location.hash = this.fragment = fragment;
745
+ if (this.iframe && (fragment != this.getFragment(this.iframe.location))) {
746
+ this.iframe.document.open().close();
747
+ this.iframe.location.hash = fragment;
748
+ }
749
+ }
750
+
751
+ });
752
+
753
+ // Backbone.View
754
+ // -------------
755
+
756
+ // Creating a Backbone.View creates its initial element outside of the DOM,
757
+ // if an existing element is not provided...
758
+ Backbone.View = function(options) {
759
+ this._configure(options || {});
760
+ this._ensureElement();
761
+ this.delegateEvents();
762
+ this.initialize(options);
763
+ };
764
+
765
+ // Element lookup, scoped to DOM elements within the current view.
766
+ // This should be prefered to global lookups, if you're dealing with
767
+ // a specific view.
768
+ var selectorDelegate = function(selector) {
769
+ return $(selector, this.el);
770
+ };
771
+
772
+ // Cached regex to split keys for `delegate`.
773
+ var eventSplitter = /^(\w+)\s*(.*)$/;
774
+
775
+ // Set up all inheritable **Backbone.View** properties and methods.
776
+ _.extend(Backbone.View.prototype, Backbone.Events, {
777
+
778
+ // The default `tagName` of a View's element is `"div"`.
779
+ tagName : 'div',
780
+
781
+ // Attach the `selectorDelegate` function as the `$` property.
782
+ $ : selectorDelegate,
783
+
784
+ // Initialize is an empty function by default. Override it with your own
785
+ // initialization logic.
786
+ initialize : function(){},
787
+
788
+ // **render** is the core function that your view should override, in order
789
+ // to populate its element (`this.el`), with the appropriate HTML. The
790
+ // convention is for **render** to always return `this`.
791
+ render : function() {
792
+ return this;
793
+ },
794
+
795
+ // Remove this view from the DOM. Note that the view isn't present in the
796
+ // DOM by default, so calling this method may be a no-op.
797
+ remove : function() {
798
+ $(this.el).remove();
799
+ return this;
800
+ },
801
+
802
+ // For small amounts of DOM Elements, where a full-blown template isn't
803
+ // needed, use **make** to manufacture elements, one at a time.
804
+ //
805
+ // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
806
+ //
807
+ make : function(tagName, attributes, content) {
808
+ var el = document.createElement(tagName);
809
+ if (attributes) $(el).attr(attributes);
810
+ if (content) $(el).html(content);
811
+ return el;
812
+ },
813
+
814
+ // Set callbacks, where `this.callbacks` is a hash of
815
+ //
816
+ // *{"event selector": "callback"}*
817
+ //
818
+ // {
819
+ // 'mousedown .title': 'edit',
820
+ // 'click .button': 'save'
821
+ // }
822
+ //
823
+ // pairs. Callbacks will be bound to the view, with `this` set properly.
824
+ // Uses event delegation for efficiency.
825
+ // Omitting the selector binds the event to `this.el`.
826
+ // This only works for delegate-able events: not `focus`, `blur`, and
827
+ // not `change`, `submit`, and `reset` in Internet Explorer.
828
+ delegateEvents : function(events) {
829
+ if (!(events || (events = this.events))) return;
830
+ $(this.el).unbind();
831
+ for (var key in events) {
832
+ var methodName = events[key];
833
+ var match = key.match(eventSplitter);
834
+ var eventName = match[1], selector = match[2];
835
+ var method = _.bind(this[methodName], this);
836
+ if (selector === '') {
837
+ $(this.el).bind(eventName, method);
838
+ } else {
839
+ $(this.el).delegate(selector, eventName, method);
840
+ }
841
+ }
842
+ },
843
+
844
+ // Performs the initial configuration of a View with a set of options.
845
+ // Keys with special meaning *(model, collection, id, className)*, are
846
+ // attached directly to the view.
847
+ _configure : function(options) {
848
+ if (this.options) options = _.extend({}, this.options, options);
849
+ if (options.model) this.model = options.model;
850
+ if (options.collection) this.collection = options.collection;
851
+ if (options.el) this.el = options.el;
852
+ if (options.id) this.id = options.id;
853
+ if (options.className) this.className = options.className;
854
+ if (options.tagName) this.tagName = options.tagName;
855
+ this.options = options;
856
+ },
857
+
858
+ // Ensure that the View has a DOM element to render into.
859
+ _ensureElement : function() {
860
+ if (this.el) return;
861
+ var attrs = {};
862
+ if (this.id) attrs.id = this.id;
863
+ if (this.className) attrs["class"] = this.className;
864
+ this.el = this.make(this.tagName, attrs);
865
+ }
866
+
867
+ });
868
+
869
+ // The self-propagating extend function that Backbone classes use.
870
+ var extend = function (protoProps, classProps) {
871
+ var child = inherits(this, protoProps, classProps);
872
+ child.extend = extend;
873
+ return child;
874
+ };
875
+
876
+ // Set up inheritance for the model, collection, and view.
877
+ Backbone.Model.extend = Backbone.Collection.extend =
878
+ Backbone.Controller.extend = Backbone.View.extend = extend;
879
+
880
+ // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
881
+ var methodMap = {
882
+ 'create': 'POST',
883
+ 'update': 'PUT',
884
+ 'delete': 'DELETE',
885
+ 'read' : 'GET'
886
+ };
887
+
888
+ // Backbone.sync
889
+ // -------------
890
+
891
+ // Override this function to change the manner in which Backbone persists
892
+ // models to the server. You will be passed the type of request, and the
893
+ // model in question. By default, uses makes a RESTful Ajax request
894
+ // to the model's `url()`. Some possible customizations could be:
895
+ //
896
+ // * Use `setTimeout` to batch rapid-fire updates into a single request.
897
+ // * Send up the models as XML instead of JSON.
898
+ // * Persist models via WebSockets instead of Ajax.
899
+ //
900
+ // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
901
+ // as `POST`, with a `_method` parameter containing the true HTTP method,
902
+ // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
903
+ // `application/json` with the model in a param named `model`.
904
+ // Useful when interfacing with server-side languages like **PHP** that make
905
+ // it difficult to read the body of `PUT` requests.
906
+ Backbone.sync = function(method, model, success, error) {
907
+ var type = methodMap[method];
908
+ var modelJSON = (method === 'create' || method === 'update') ?
909
+ JSON.stringify(model.toJSON()) : null;
910
+
911
+ // Default JSON-request options.
912
+ var params = {
913
+ url: getUrl(model),
914
+ type: type,
915
+ contentType: 'application/json',
916
+ data: modelJSON,
917
+ dataType: 'json',
918
+ processData: false,
919
+ success: success,
920
+ error: error
921
+ };
922
+
923
+ // For older servers, emulate JSON by encoding the request into an HTML-form.
924
+ if (Backbone.emulateJSON) {
925
+ params.contentType = 'application/x-www-form-urlencoded';
926
+ params.processData = true;
927
+ params.data = modelJSON ? {model : modelJSON} : {};
928
+ }
929
+
930
+ // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
931
+ // And an `X-HTTP-Method-Override` header.
932
+ if (Backbone.emulateHTTP) {
933
+ if (type === 'PUT' || type === 'DELETE') {
934
+ if (Backbone.emulateJSON) params.data._method = type;
935
+ params.type = 'POST';
936
+ params.beforeSend = function(xhr) {
937
+ xhr.setRequestHeader("X-HTTP-Method-Override", type);
938
+ };
939
+ }
940
+ }
941
+
942
+ // Make the request.
943
+ $.ajax(params);
944
+ };
945
+
946
+ // Helpers
947
+ // -------
948
+
949
+ // Shared empty constructor function to aid in prototype-chain creation.
950
+ var ctor = function(){};
951
+
952
+ // Helper function to correctly set up the prototype chain, for subclasses.
953
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and
954
+ // class properties to be extended.
955
+ var inherits = function(parent, protoProps, staticProps) {
956
+ var child;
957
+
958
+ // The constructor function for the new subclass is either defined by you
959
+ // (the "constructor" property in your `extend` definition), or defaulted
960
+ // by us to simply call `super()`.
961
+ if (protoProps && protoProps.hasOwnProperty('constructor')) {
962
+ child = protoProps.constructor;
963
+ } else {
964
+ child = function(){ return parent.apply(this, arguments); };
965
+ }
966
+
967
+ // Set the prototype chain to inherit from `parent`, without calling
968
+ // `parent`'s constructor function.
969
+ ctor.prototype = parent.prototype;
970
+ child.prototype = new ctor();
971
+
972
+ // Add prototype properties (instance properties) to the subclass,
973
+ // if supplied.
974
+ if (protoProps) _.extend(child.prototype, protoProps);
975
+
976
+ // Add static properties to the constructor function, if supplied.
977
+ if (staticProps) _.extend(child, staticProps);
978
+
979
+ // Correctly set child's `prototype.constructor`, for `instanceof`.
980
+ child.prototype.constructor = child;
981
+
982
+ // Set a convenience property in case the parent's prototype is needed later.
983
+ child.__super__ = parent.prototype;
984
+
985
+ return child;
986
+ };
987
+
988
+ // Helper function to get a URL from a Model or Collection as a property
989
+ // or as a function.
990
+ var getUrl = function(object) {
991
+ if (!(object && object.url)) throw new Error("A 'url' property or function must be specified");
992
+ return _.isFunction(object.url) ? object.url() : object.url;
993
+ };
994
+
995
+ // Wrap an optional error callback with a fallback error event.
996
+ var wrapError = function(onError, model, options) {
997
+ return function(resp) {
998
+ if (onError) {
999
+ onError(model, resp);
1000
+ } else {
1001
+ model.trigger('error', model, resp, options);
1002
+ }
1003
+ };
1004
+ };
1005
+
1006
+ // Helper function to escape a string for HTML rendering.
1007
+ var escapeHTML = function(string) {
1008
+ return string.replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1009
+ };
1010
+
1011
+ })();