Switching old client with the new
[KiwiIRC.git] / client / backbone-git.js
1 // Backbone.js 0.5.3
2 // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
3 // Backbone may be freely distributed under the MIT license.
4 // For all details and documentation:
5 // http://backbonejs.org
6
7 (function(){
8
9 // Initial Setup
10 // -------------
11
12 // Save a reference to the global object (`window` in the browser, `global`
13 // on the server).
14 var root = this;
15
16 // Save the previous value of the `Backbone` variable, so that it can be
17 // restored later on, if `noConflict` is used.
18 var previousBackbone = root.Backbone;
19
20 // Create a local reference to slice/splice.
21 var slice = Array.prototype.slice;
22 var splice = Array.prototype.splice;
23
24 // The top-level namespace. All public Backbone classes and modules will
25 // be attached to this. Exported for both CommonJS and the browser.
26 var Backbone;
27 if (typeof exports !== 'undefined') {
28 Backbone = exports;
29 } else {
30 Backbone = root.Backbone = {};
31 }
32
33 // Current version of the library. Keep in sync with `package.json`.
34 Backbone.VERSION = '0.5.3';
35
36 // Require Underscore, if we're on the server, and it's not already present.
37 var _ = root._;
38 if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
39
40 // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
41 var $ = root.jQuery || root.Zepto || root.ender;
42
43 // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
44 // to its previous owner. Returns a reference to this Backbone object.
45 Backbone.noConflict = function() {
46 root.Backbone = previousBackbone;
47 return this;
48 };
49
50 // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
51 // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
52 // set a `X-Http-Method-Override` header.
53 Backbone.emulateHTTP = false;
54
55 // Turn on `emulateJSON` to support legacy servers that can't deal with direct
56 // `application/json` requests ... will encode the body as
57 // `application/x-www-form-urlencoded` instead and will send the model in a
58 // form param named `model`.
59 Backbone.emulateJSON = false;
60
61 // Backbone.Events
62 // -----------------
63
64 // A module that can be mixed in to *any object* in order to provide it with
65 // custom events. You may bind with `on` or remove with `off` callback functions
66 // to an event; trigger`-ing an event fires all callbacks in succession.
67 //
68 // var object = {};
69 // _.extend(object, Backbone.Events);
70 // object.on('expand', function(){ alert('expanded'); });
71 // object.trigger('expand');
72 //
73 Backbone.Events = {
74
75 // Bind an event, specified by a string name, `ev`, to a `callback`
76 // function. Passing `"all"` will bind the callback to all events fired.
77 on: function(events, callback, context) {
78 var ev;
79 events = events.split(/\s+/);
80 var calls = this._callbacks || (this._callbacks = {});
81 while (ev = events.shift()) {
82 // Create an immutable callback list, allowing traversal during
83 // modification. The tail is an empty object that will always be used
84 // as the next node.
85 var list = calls[ev] || (calls[ev] = {});
86 var tail = list.tail || (list.tail = list.next = {});
87 tail.callback = callback;
88 tail.context = context;
89 list.tail = tail.next = {};
90 }
91 return this;
92 },
93
94 // Remove one or many callbacks. If `context` is null, removes all callbacks
95 // with that function. If `callback` is null, removes all callbacks for the
96 // event. If `ev` is null, removes all bound callbacks for all events.
97 off: function(events, callback, context) {
98 var ev, calls, node;
99 if (!events) {
100 delete this._callbacks;
101 } else if (calls = this._callbacks) {
102 events = events.split(/\s+/);
103 while (ev = events.shift()) {
104 node = calls[ev];
105 delete calls[ev];
106 if (!callback || !node) continue;
107 // Create a new list, omitting the indicated event/context pairs.
108 while ((node = node.next) && node.next) {
109 if (node.callback === callback &&
110 (!context || node.context === context)) continue;
111 this.on(ev, node.callback, node.context);
112 }
113 }
114 }
115 return this;
116 },
117
118 // Trigger an event, firing all bound callbacks. Callbacks are passed the
119 // same arguments as `trigger` is, apart from the event name.
120 // Listening for `"all"` passes the true event name as the first argument.
121 trigger: function(events) {
122 var event, node, calls, tail, args, all, rest;
123 if (!(calls = this._callbacks)) return this;
124 all = calls['all'];
125 (events = events.split(/\s+/)).push(null);
126 // Save references to the current heads & tails.
127 while (event = events.shift()) {
128 if (all) events.push({next: all.next, tail: all.tail, event: event});
129 if (!(node = calls[event])) continue;
130 events.push({next: node.next, tail: node.tail});
131 }
132 // Traverse each list, stopping when the saved tail is reached.
133 rest = slice.call(arguments, 1);
134 while (node = events.pop()) {
135 tail = node.tail;
136 args = node.event ? [node.event].concat(rest) : rest;
137 while ((node = node.next) !== tail) {
138 node.callback.apply(node.context || this, args);
139 }
140 }
141 return this;
142 }
143
144 };
145
146 // Aliases for backwards compatibility.
147 Backbone.Events.bind = Backbone.Events.on;
148 Backbone.Events.unbind = Backbone.Events.off;
149
150 // Backbone.Model
151 // --------------
152
153 // Create a new model, with defined attributes. A client id (`cid`)
154 // is automatically generated and assigned for you.
155 Backbone.Model = function(attributes, options) {
156 var defaults;
157 attributes || (attributes = {});
158 if (options && options.parse) attributes = this.parse(attributes);
159 if (defaults = getValue(this, 'defaults')) {
160 attributes = _.extend({}, defaults, attributes);
161 }
162 if (options && options.collection) this.collection = options.collection;
163 this.attributes = {};
164 this._escapedAttributes = {};
165 this.cid = _.uniqueId('c');
166 if (!this.set(attributes, {silent: true})) {
167 throw new Error("Can't create an invalid model");
168 }
169 this._changed = false;
170 this._previousAttributes = _.clone(this.attributes);
171 this.initialize.apply(this, arguments);
172 };
173
174 // Attach all inheritable methods to the Model prototype.
175 _.extend(Backbone.Model.prototype, Backbone.Events, {
176
177 // Has the item been changed since the last `"change"` event?
178 _changed: false,
179
180 // The default name for the JSON `id` attribute is `"id"`. MongoDB and
181 // CouchDB users may want to set this to `"_id"`.
182 idAttribute: 'id',
183
184 // Initialize is an empty function by default. Override it with your own
185 // initialization logic.
186 initialize: function(){},
187
188 // Return a copy of the model's `attributes` object.
189 toJSON: function() {
190 return _.clone(this.attributes);
191 },
192
193 // Get the value of an attribute.
194 get: function(attr) {
195 return this.attributes[attr];
196 },
197
198 // Get the HTML-escaped value of an attribute.
199 escape: function(attr) {
200 var html;
201 if (html = this._escapedAttributes[attr]) return html;
202 var val = this.attributes[attr];
203 return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
204 },
205
206 // Returns `true` if the attribute contains a value that is not null
207 // or undefined.
208 has: function(attr) {
209 return this.attributes[attr] != null;
210 },
211
212 // Set a hash of model attributes on the object, firing `"change"` unless
213 // you choose to silence it.
214 set: function(key, value, options) {
215 var attrs, attr, val;
216 if (_.isObject(key) || key == null) {
217 attrs = key;
218 options = value;
219 } else {
220 attrs = {};
221 attrs[key] = value;
222 }
223
224 // Extract attributes and options.
225 options || (options = {});
226 if (!attrs) return this;
227 if (attrs instanceof Backbone.Model) attrs = attrs.attributes;
228 if (options.unset) for (var attr in attrs) attrs[attr] = void 0;
229 var now = this.attributes, escaped = this._escapedAttributes;
230
231 // Run validation.
232 if (this.validate && !this._performValidation(attrs, options)) return false;
233
234 // Check for changes of `id`.
235 if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
236
237 // We're about to start triggering change events.
238 var alreadyChanging = this._changing;
239 this._changing = true;
240
241 // Update attributes.
242 var changes = {};
243 for (attr in attrs) {
244 val = attrs[attr];
245 if (!_.isEqual(now[attr], val) || (options.unset && (attr in now))) {
246 delete escaped[attr];
247 this._changed = true;
248 changes[attr] = val;
249 }
250 options.unset ? delete now[attr] : now[attr] = val;
251 }
252
253 // Fire `change:attribute` events.
254 for (var attr in changes) {
255 if (!options.silent) this.trigger('change:' + attr, this, changes[attr], options);
256 }
257
258 // Fire the `"change"` event, if the model has been changed.
259 if (!alreadyChanging) {
260 if (!options.silent && this._changed) this.change(options);
261 this._changing = false;
262 }
263 return this;
264 },
265
266 // Remove an attribute from the model, firing `"change"` unless you choose
267 // to silence it. `unset` is a noop if the attribute doesn't exist.
268 unset: function(attr, options) {
269 (options || (options = {})).unset = true;
270 return this.set(attr, null, options);
271 },
272
273 // Clear all attributes on the model, firing `"change"` unless you choose
274 // to silence it.
275 clear: function(options) {
276 (options || (options = {})).unset = true;
277 return this.set(_.clone(this.attributes), options);
278 },
279
280 // Fetch the model from the server. If the server's representation of the
281 // model differs from its current attributes, they will be overriden,
282 // triggering a `"change"` event.
283 fetch: function(options) {
284 options = options ? _.clone(options) : {};
285 var model = this;
286 var success = options.success;
287 options.success = function(resp, status, xhr) {
288 if (!model.set(model.parse(resp, xhr), options)) return false;
289 if (success) success(model, resp);
290 };
291 options.error = Backbone.wrapError(options.error, model, options);
292 return (this.sync || Backbone.sync).call(this, 'read', this, options);
293 },
294
295 // Set a hash of model attributes, and sync the model to the server.
296 // If the server returns an attributes hash that differs, the model's
297 // state will be `set` again.
298 save: function(key, value, options) {
299 var attrs;
300 if (_.isObject(key) || key == null) {
301 attrs = key;
302 options = value;
303 } else {
304 attrs = {};
305 attrs[key] = value;
306 }
307
308 options = options ? _.clone(options) : {};
309 if (attrs && !this[options.wait ? '_performValidation' : 'set'](attrs, options)) return false;
310 var model = this;
311 var success = options.success;
312 options.success = function(resp, status, xhr) {
313 var serverAttrs = model.parse(resp, xhr);
314 if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
315 if (!model.set(serverAttrs, options)) return false;
316 if (success) {
317 success(model, resp);
318 } else {
319 model.trigger('sync', model, resp, options);
320 }
321 };
322 options.error = Backbone.wrapError(options.error, model, options);
323 var method = this.isNew() ? 'create' : 'update';
324 return (this.sync || Backbone.sync).call(this, method, this, options);
325 },
326
327 // Destroy this model on the server if it was already persisted.
328 // Optimistically removes the model from its collection, if it has one.
329 // If `wait: true` is passed, waits for the server to respond before removal.
330 destroy: function(options) {
331 options = options ? _.clone(options) : {};
332 var model = this;
333 var success = options.success;
334
335 var triggerDestroy = function() {
336 model.trigger('destroy', model, model.collection, options);
337 };
338
339 if (this.isNew()) return triggerDestroy();
340 options.success = function(resp) {
341 if (options.wait) triggerDestroy();
342 if (success) {
343 success(model, resp);
344 } else {
345 model.trigger('sync', model, resp, options);
346 }
347 };
348 options.error = Backbone.wrapError(options.error, model, options);
349 var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
350 if (!options.wait) triggerDestroy();
351 return xhr;
352 },
353
354 // Default URL for the model's representation on the server -- if you're
355 // using Backbone's restful methods, override this to change the endpoint
356 // that will be called.
357 url: function() {
358 var base = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || urlError();
359 if (this.isNew()) return base;
360 return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
361 },
362
363 // **parse** converts a response into the hash of attributes to be `set` on
364 // the model. The default implementation is just to pass the response along.
365 parse: function(resp, xhr) {
366 return resp;
367 },
368
369 // Create a new model with identical attributes to this one.
370 clone: function() {
371 return new this.constructor(this.attributes);
372 },
373
374 // A model is new if it has never been saved to the server, and lacks an id.
375 isNew: function() {
376 return this.id == null;
377 },
378
379 // Call this method to manually fire a `change` event for this model.
380 // Calling this will cause all objects observing the model to update.
381 change: function(options) {
382 this.trigger('change', this, options);
383 this._previousAttributes = _.clone(this.attributes);
384 this._changed = false;
385 },
386
387 // Determine if the model has changed since the last `"change"` event.
388 // If you specify an attribute name, determine if that attribute has changed.
389 hasChanged: function(attr) {
390 if (attr) return !_.isEqual(this._previousAttributes[attr], this.attributes[attr]);
391 return this._changed;
392 },
393
394 // Return an object containing all the attributes that have changed, or
395 // false if there are no changed attributes. Useful for determining what
396 // parts of a view need to be updated and/or what attributes need to be
397 // persisted to the server. Unset attributes will be set to undefined.
398 changedAttributes: function(now) {
399 if (!this._changed) return false;
400 now || (now = this.attributes);
401 var changed = false, old = this._previousAttributes;
402 for (var attr in now) {
403 if (_.isEqual(old[attr], now[attr])) continue;
404 (changed || (changed = {}))[attr] = now[attr];
405 }
406 for (var attr in old) {
407 if (!(attr in now)) (changed || (changed = {}))[attr] = void 0;
408 }
409 return changed;
410 },
411
412 // Get the previous value of an attribute, recorded at the time the last
413 // `"change"` event was fired.
414 previous: function(attr) {
415 if (!attr || !this._previousAttributes) return null;
416 return this._previousAttributes[attr];
417 },
418
419 // Get all of the attributes of the model at the time of the previous
420 // `"change"` event.
421 previousAttributes: function() {
422 return _.clone(this._previousAttributes);
423 },
424
425 // Run validation against a set of incoming attributes, returning `true`
426 // if all is well. If a specific `error` callback has been passed,
427 // call that instead of firing the general `"error"` event.
428 _performValidation: function(attrs, options) {
429 var newAttrs = _.extend({}, this.attributes, attrs);
430 var error = this.validate(newAttrs, options);
431 if (error) {
432 if (options.error) {
433 options.error(this, error, options);
434 } else {
435 this.trigger('error', this, error, options);
436 }
437 return false;
438 }
439 return true;
440 }
441
442 });
443
444 // Backbone.Collection
445 // -------------------
446
447 // Provides a standard collection class for our sets of models, ordered
448 // or unordered. If a `comparator` is specified, the Collection will maintain
449 // its models in sort order, as they're added and removed.
450 Backbone.Collection = function(models, options) {
451 options || (options = {});
452 if (options.comparator) this.comparator = options.comparator;
453 this._reset();
454 this.initialize.apply(this, arguments);
455 if (models) this.reset(models, {silent: true, parse: options.parse});
456 };
457
458 // Define the Collection's inheritable methods.
459 _.extend(Backbone.Collection.prototype, Backbone.Events, {
460
461 // The default model for a collection is just a **Backbone.Model**.
462 // This should be overridden in most cases.
463 model: Backbone.Model,
464
465 // Initialize is an empty function by default. Override it with your own
466 // initialization logic.
467 initialize: function(){},
468
469 // The JSON representation of a Collection is an array of the
470 // models' attributes.
471 toJSON: function() {
472 return this.map(function(model){ return model.toJSON(); });
473 },
474
475 // Add a model, or list of models to the set. Pass **silent** to avoid
476 // firing the `add` event for every new model.
477 add: function(models, options) {
478 var i, index, length, model, cids = {};
479 options || (options = {});
480 models = _.isArray(models) ? models.slice() : [models];
481
482 // Begin by turning bare objects into model references, and preventing
483 // invalid models or duplicate models from being added.
484 for (i = 0, length = models.length; i < length; i++) {
485 if (!(model = models[i] = this._prepareModel(models[i], options))) {
486 throw new Error("Can't add an invalid model to a collection");
487 }
488 var hasId = model.id != null;
489 if (this._byCid[model.cid] || (hasId && this._byId[model.id])) {
490 throw new Error("Can't add the same model to a collection twice");
491 }
492 }
493
494 // Listen to added models' events, and index models for lookup by
495 // `id` and by `cid`.
496 for (i = 0; i < length; i++) {
497 (model = models[i]).on('all', this._onModelEvent, this);
498 this._byCid[model.cid] = model;
499 if (model.id != null) this._byId[model.id] = model;
500 cids[model.cid] = true;
501 }
502
503 // Insert models into the collection, re-sorting if needed, and triggering
504 // `add` events unless silenced.
505 this.length += length;
506 index = options.at != null ? options.at : this.models.length;
507 splice.apply(this.models, [index, 0].concat(models));
508 if (this.comparator) this.sort({silent: true});
509 if (options.silent) return this;
510 for (i = 0, length = this.models.length; i < length; i++) {
511 if (!cids[(model = this.models[i]).cid]) continue;
512 options.index = i;
513 model.trigger('add', model, this, options);
514 }
515 return this;
516 },
517
518 // Remove a model, or a list of models from the set. Pass silent to avoid
519 // firing the `remove` event for every model removed.
520 remove: function(models, options) {
521 var i, l, index, model;
522 options || (options = {});
523 models = _.isArray(models) ? models.slice() : [models];
524 for (i = 0, l = models.length; i < l; i++) {
525 model = this.getByCid(models[i]) || this.get(models[i]);
526 if (!model) continue;
527 delete this._byId[model.id];
528 delete this._byCid[model.cid];
529 index = this.indexOf(model);
530 this.models.splice(index, 1);
531 this.length--;
532 if (!options.silent) {
533 options.index = index;
534 model.trigger('remove', model, this, options);
535 }
536 this._removeReference(model);
537 }
538 return this;
539 },
540
541 // Get a model from the set by id.
542 get: function(id) {
543 if (id == null) return null;
544 return this._byId[id.id != null ? id.id : id];
545 },
546
547 // Get a model from the set by client id.
548 getByCid: function(cid) {
549 return cid && this._byCid[cid.cid || cid];
550 },
551
552 // Get the model at the given index.
553 at: function(index) {
554 return this.models[index];
555 },
556
557 // Force the collection to re-sort itself. You don't need to call this under
558 // normal circumstances, as the set will maintain sort order as each item
559 // is added.
560 sort: function(options) {
561 options || (options = {});
562 if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
563 var boundComparator = _.bind(this.comparator, this);
564 if (this.comparator.length == 1) {
565 this.models = this.sortBy(boundComparator);
566 } else {
567 this.models.sort(boundComparator);
568 }
569 if (!options.silent) this.trigger('reset', this, options);
570 return this;
571 },
572
573 // Pluck an attribute from each model in the collection.
574 pluck: function(attr) {
575 return _.map(this.models, function(model){ return model.get(attr); });
576 },
577
578 // When you have more items than you want to add or remove individually,
579 // you can reset the entire set with a new list of models, without firing
580 // any `add` or `remove` events. Fires `reset` when finished.
581 reset: function(models, options) {
582 models || (models = []);
583 options || (options = {});
584 for (var i = 0, l = this.models.length; i < l; i++) {
585 this._removeReference(this.models[i]);
586 }
587 this._reset();
588 this.add(models, {silent: true, parse: options.parse});
589 if (!options.silent) this.trigger('reset', this, options);
590 return this;
591 },
592
593 // Fetch the default set of models for this collection, resetting the
594 // collection when they arrive. If `add: true` is passed, appends the
595 // models to the collection instead of resetting.
596 fetch: function(options) {
597 options = options ? _.clone(options) : {};
598 if (options.parse === undefined) options.parse = true;
599 var collection = this;
600 var success = options.success;
601 options.success = function(resp, status, xhr) {
602 collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
603 if (success) success(collection, resp);
604 };
605 options.error = Backbone.wrapError(options.error, collection, options);
606 return (this.sync || Backbone.sync).call(this, 'read', this, options);
607 },
608
609 // Create a new instance of a model in this collection. Add the model to the
610 // collection immediately, unless `wait: true` is passed, in which case we
611 // wait for the server to agree.
612 create: function(model, options) {
613 var coll = this;
614 options = options ? _.clone(options) : {};
615 model = this._prepareModel(model, options);
616 if (!model) return false;
617 if (!options.wait) coll.add(model, options);
618 var success = options.success;
619 options.success = function(nextModel, resp, xhr) {
620 if (options.wait) coll.add(nextModel, options);
621 if (success) {
622 success(nextModel, resp);
623 } else {
624 nextModel.trigger('sync', model, resp, options);
625 }
626 };
627 model.save(null, options);
628 return model;
629 },
630
631 // **parse** converts a response into a list of models to be added to the
632 // collection. The default implementation is just to pass it through.
633 parse: function(resp, xhr) {
634 return resp;
635 },
636
637 // Proxy to _'s chain. Can't be proxied the same way the rest of the
638 // underscore methods are proxied because it relies on the underscore
639 // constructor.
640 chain: function () {
641 return _(this.models).chain();
642 },
643
644 // Reset all internal state. Called when the collection is reset.
645 _reset: function(options) {
646 this.length = 0;
647 this.models = [];
648 this._byId = {};
649 this._byCid = {};
650 },
651
652 // Prepare a model or hash of attributes to be added to this collection.
653 _prepareModel: function(model, options) {
654 if (!(model instanceof Backbone.Model)) {
655 var attrs = model;
656 options.collection = this;
657 model = new this.model(attrs, options);
658 if (model.validate && !model._performValidation(model.attributes, options)) model = false;
659 } else if (!model.collection) {
660 model.collection = this;
661 }
662 return model;
663 },
664
665 // Internal method to remove a model's ties to a collection.
666 _removeReference: function(model) {
667 if (this == model.collection) {
668 delete model.collection;
669 }
670 model.off('all', this._onModelEvent, this);
671 },
672
673 // Internal method called every time a model in the set fires an event.
674 // Sets need to update their indexes when models change ids. All other
675 // events simply proxy through. "add" and "remove" events that originate
676 // in other collections are ignored.
677 _onModelEvent: function(ev, model, collection, options) {
678 if ((ev == 'add' || ev == 'remove') && collection != this) return;
679 if (ev == 'destroy') {
680 this.remove(model, options);
681 }
682 if (model && ev === 'change:' + model.idAttribute) {
683 delete this._byId[model.previous(model.idAttribute)];
684 this._byId[model.id] = model;
685 }
686 this.trigger.apply(this, arguments);
687 }
688
689 });
690
691 // Underscore methods that we want to implement on the Collection.
692 var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
693 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
694 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
695 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
696 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
697
698 // Mix in each Underscore method as a proxy to `Collection#models`.
699 _.each(methods, function(method) {
700 Backbone.Collection.prototype[method] = function() {
701 return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
702 };
703 });
704
705 // Backbone.Router
706 // -------------------
707
708 // Routers map faux-URLs to actions, and fire events when routes are
709 // matched. Creating a new one sets its `routes` hash, if not set statically.
710 Backbone.Router = function(options) {
711 options || (options = {});
712 if (options.routes) this.routes = options.routes;
713 this._bindRoutes();
714 this.initialize.apply(this, arguments);
715 };
716
717 // Cached regular expressions for matching named param parts and splatted
718 // parts of route strings.
719 var namedParam = /:\w+/g;
720 var splatParam = /\*\w+/g;
721 var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
722
723 // Set up all inheritable **Backbone.Router** properties and methods.
724 _.extend(Backbone.Router.prototype, Backbone.Events, {
725
726 // Initialize is an empty function by default. Override it with your own
727 // initialization logic.
728 initialize: function(){},
729
730 // Manually bind a single named route to a callback. For example:
731 //
732 // this.route('search/:query/p:num', 'search', function(query, num) {
733 // ...
734 // });
735 //
736 route: function(route, name, callback) {
737 Backbone.history || (Backbone.history = new Backbone.History);
738 if (!_.isRegExp(route)) route = this._routeToRegExp(route);
739 if (!callback) callback = this[name];
740 Backbone.history.route(route, _.bind(function(fragment) {
741 var args = this._extractParameters(route, fragment);
742 callback && callback.apply(this, args);
743 this.trigger.apply(this, ['route:' + name].concat(args));
744 Backbone.history.trigger('route', this, name, args);
745 }, this));
746 },
747
748 // Simple proxy to `Backbone.history` to save a fragment into the history.
749 navigate: function(fragment, options) {
750 Backbone.history.navigate(fragment, options);
751 },
752
753 // Bind all defined routes to `Backbone.history`. We have to reverse the
754 // order of the routes here to support behavior where the most general
755 // routes can be defined at the bottom of the route map.
756 _bindRoutes: function() {
757 if (!this.routes) return;
758 var routes = [];
759 for (var route in this.routes) {
760 routes.unshift([route, this.routes[route]]);
761 }
762 for (var i = 0, l = routes.length; i < l; i++) {
763 this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
764 }
765 },
766
767 // Convert a route string into a regular expression, suitable for matching
768 // against the current location hash.
769 _routeToRegExp: function(route) {
770 route = route.replace(escapeRegExp, '\\$&')
771 .replace(namedParam, '([^\/]+)')
772 .replace(splatParam, '(.*?)');
773 return new RegExp('^' + route + '$');
774 },
775
776 // Given a route, and a URL fragment that it matches, return the array of
777 // extracted parameters.
778 _extractParameters: function(route, fragment) {
779 return route.exec(fragment).slice(1);
780 }
781
782 });
783
784 // Backbone.History
785 // ----------------
786
787 // Handles cross-browser history management, based on URL fragments. If the
788 // browser does not support `onhashchange`, falls back to polling.
789 Backbone.History = function() {
790 this.handlers = [];
791 _.bindAll(this, 'checkUrl');
792 };
793
794 // Cached regex for cleaning leading hashes and slashes .
795 var routeStripper = /^[#\/]/;
796
797 // Cached regex for detecting MSIE.
798 var isExplorer = /msie [\w.]+/;
799
800 // Has the history handling already been started?
801 var historyStarted = false;
802
803 // Set up all inheritable **Backbone.History** properties and methods.
804 _.extend(Backbone.History.prototype, Backbone.Events, {
805
806 // The default interval to poll for hash changes, if necessary, is
807 // twenty times a second.
808 interval: 50,
809
810 // Get the cross-browser normalized URL fragment, either from the URL,
811 // the hash, or the override.
812 getFragment: function(fragment, forcePushState) {
813 if (fragment == null) {
814 if (this._hasPushState || forcePushState) {
815 fragment = window.location.pathname;
816 var search = window.location.search;
817 if (search) fragment += search;
818 } else {
819 fragment = window.location.hash;
820 }
821 }
822 fragment = decodeURIComponent(fragment.replace(routeStripper, ''));
823 if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
824 return fragment;
825 },
826
827 // Start the hash change handling, returning `true` if the current URL matches
828 // an existing route, and `false` otherwise.
829 start: function(options) {
830
831 // Figure out the initial configuration. Do we need an iframe?
832 // Is pushState desired ... is it available?
833 if (historyStarted) throw new Error("Backbone.history has already been started");
834 this.options = _.extend({}, {root: '/'}, this.options, options);
835 this._wantsHashChange = this.options.hashChange !== false;
836 this._wantsPushState = !!this.options.pushState;
837 this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
838 var fragment = this.getFragment();
839 var docMode = document.documentMode;
840 var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
841 if (oldIE) {
842 this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
843 this.navigate(fragment);
844 }
845
846 // Depending on whether we're using pushState or hashes, and whether
847 // 'onhashchange' is supported, determine how we check the URL state.
848 if (this._hasPushState) {
849 $(window).bind('popstate', this.checkUrl);
850 } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
851 $(window).bind('hashchange', this.checkUrl);
852 } else if (this._wantsHashChange) {
853 this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
854 }
855
856 // Determine if we need to change the base url, for a pushState link
857 // opened by a non-pushState browser.
858 this.fragment = fragment;
859 historyStarted = true;
860 var loc = window.location;
861 var atRoot = loc.pathname == this.options.root;
862
863 // If we've started off with a route from a `pushState`-enabled browser,
864 // but we're currently in a browser that doesn't support it...
865 if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
866 this.fragment = this.getFragment(null, true);
867 window.location.replace(this.options.root + '#' + this.fragment);
868 // Return immediately as browser will do redirect to new url
869 return true;
870
871 // Or if we've started out with a hash-based route, but we're currently
872 // in a browser where it could be `pushState`-based instead...
873 } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
874 this.fragment = loc.hash.replace(routeStripper, '');
875 window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
876 }
877
878 if (!this.options.silent) {
879 return this.loadUrl();
880 }
881 },
882
883 // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
884 // but possibly useful for unit testing Routers.
885 stop: function() {
886 $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
887 clearInterval(this._checkUrlInterval);
888 historyStarted = false;
889 },
890
891 // Add a route to be tested when the fragment changes. Routes added later
892 // may override previous routes.
893 route: function(route, callback) {
894 this.handlers.unshift({route: route, callback: callback});
895 },
896
897 // Checks the current URL to see if it has changed, and if it has,
898 // calls `loadUrl`, normalizing across the hidden iframe.
899 checkUrl: function(e) {
900 var current = this.getFragment();
901 if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
902 if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
903 if (this.iframe) this.navigate(current);
904 this.loadUrl() || this.loadUrl(window.location.hash);
905 },
906
907 // Attempt to load the current URL fragment. If a route succeeds with a
908 // match, returns `true`. If no defined routes matches the fragment,
909 // returns `false`.
910 loadUrl: function(fragmentOverride) {
911 var fragment = this.fragment = this.getFragment(fragmentOverride);
912 var matched = _.any(this.handlers, function(handler) {
913 if (handler.route.test(fragment)) {
914 handler.callback(fragment);
915 return true;
916 }
917 });
918 return matched;
919 },
920
921 // Save a fragment into the hash history, or replace the URL state if the
922 // 'replace' option is passed. You are responsible for properly URL-encoding
923 // the fragment in advance.
924 //
925 // The options object can contain `trigger: true` if you wish to have the
926 // route callback be fired (not usually desirable), or `replace: true`, if
927 // you which to modify the current URL without adding an entry to the history.
928 navigate: function(fragment, options) {
929 if (!historyStarted) return false;
930 if (!options || options === true) options = {trigger: options};
931 var frag = (fragment || '').replace(routeStripper, '');
932 if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
933
934 // If pushState is available, we use it to set the fragment as a real URL.
935 if (this._hasPushState) {
936 if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
937 this.fragment = frag;
938 window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
939
940 // If hash changes haven't been explicitly disabled, update the hash
941 // fragment to store history.
942 } else if (this._wantsHashChange) {
943 this.fragment = frag;
944 this._updateHash(window.location, frag, options.replace);
945 if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
946 // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
947 // When replace is true, we don't want this.
948 if(!options.replace) this.iframe.document.open().close();
949 this._updateHash(this.iframe.location, frag, options.replace);
950 }
951
952 // If you've told us that you explicitly don't want fallback hashchange-
953 // based history, then `navigate` becomes a page refresh.
954 } else {
955 window.location.assign(this.options.root + fragment);
956 }
957 if (options.trigger) this.loadUrl(fragment);
958 },
959
960 // Update the hash location, either replacing the current entry, or adding
961 // a new one to the browser history.
962 _updateHash: function(location, fragment, replace) {
963 if (replace) {
964 location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
965 } else {
966 location.hash = fragment;
967 }
968 }
969 });
970
971 // Backbone.View
972 // -------------
973
974 // Creating a Backbone.View creates its initial element outside of the DOM,
975 // if an existing element is not provided...
976 Backbone.View = function(options) {
977 this.cid = _.uniqueId('view');
978 this._configure(options || {});
979 this._ensureElement();
980 this.initialize.apply(this, arguments);
981 this.delegateEvents();
982 };
983
984 // Cached regex to split keys for `delegate`.
985 var eventSplitter = /^(\S+)\s*(.*)$/;
986
987 // List of view options to be merged as properties.
988 var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
989
990 // Set up all inheritable **Backbone.View** properties and methods.
991 _.extend(Backbone.View.prototype, Backbone.Events, {
992
993 // The default `tagName` of a View's element is `"div"`.
994 tagName: 'div',
995
996 // jQuery delegate for element lookup, scoped to DOM elements within the
997 // current view. This should be prefered to global lookups where possible.
998 $: function(selector) {
999 return $(selector, this.el);
1000 },
1001
1002 // Initialize is an empty function by default. Override it with your own
1003 // initialization logic.
1004 initialize: function(){},
1005
1006 // **render** is the core function that your view should override, in order
1007 // to populate its element (`this.el`), with the appropriate HTML. The
1008 // convention is for **render** to always return `this`.
1009 render: function() {
1010 return this;
1011 },
1012
1013 // Remove this view from the DOM. Note that the view isn't present in the
1014 // DOM by default, so calling this method may be a no-op.
1015 remove: function() {
1016 this.$el.remove();
1017 return this;
1018 },
1019
1020 // For small amounts of DOM Elements, where a full-blown template isn't
1021 // needed, use **make** to manufacture elements, one at a time.
1022 //
1023 // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
1024 //
1025 make: function(tagName, attributes, content) {
1026 var el = document.createElement(tagName);
1027 if (attributes) $(el).attr(attributes);
1028 if (content) $(el).html(content);
1029 return el;
1030 },
1031
1032 // Change the view's element (`this.el` property), including event
1033 // re-delegation.
1034 setElement: function(element, delegate) {
1035 this.$el = $(element);
1036 this.el = this.$el[0];
1037 if (delegate !== false) this.delegateEvents();
1038 },
1039
1040 // Set callbacks, where `this.events` is a hash of
1041 //
1042 // *{"event selector": "callback"}*
1043 //
1044 // {
1045 // 'mousedown .title': 'edit',
1046 // 'click .button': 'save'
1047 // 'click .open': function(e) { ... }
1048 // }
1049 //
1050 // pairs. Callbacks will be bound to the view, with `this` set properly.
1051 // Uses event delegation for efficiency.
1052 // Omitting the selector binds the event to `this.el`.
1053 // This only works for delegate-able events: not `focus`, `blur`, and
1054 // not `change`, `submit`, and `reset` in Internet Explorer.
1055 delegateEvents: function(events) {
1056 if (!(events || (events = getValue(this, 'events')))) return;
1057 this.undelegateEvents();
1058 for (var key in events) {
1059 var method = events[key];
1060 if (!_.isFunction(method)) method = this[events[key]];
1061 if (!method) throw new Error('Event "' + events[key] + '" does not exist');
1062 var match = key.match(eventSplitter);
1063 var eventName = match[1], selector = match[2];
1064 method = _.bind(method, this);
1065 eventName += '.delegateEvents' + this.cid;
1066 if (selector === '') {
1067 this.$el.bind(eventName, method);
1068 } else {
1069 this.$el.delegate(selector, eventName, method);
1070 }
1071 }
1072 },
1073
1074 // Clears all callbacks previously bound to the view with `delegateEvents`.
1075 // You usually don't need to use this, but may wish to if you have multiple
1076 // Backbone views attached to the same DOM element.
1077 undelegateEvents: function() {
1078 this.$el.unbind('.delegateEvents' + this.cid);
1079 },
1080
1081 // Performs the initial configuration of a View with a set of options.
1082 // Keys with special meaning *(model, collection, id, className)*, are
1083 // attached directly to the view.
1084 _configure: function(options) {
1085 if (this.options) options = _.extend({}, this.options, options);
1086 for (var i = 0, l = viewOptions.length; i < l; i++) {
1087 var attr = viewOptions[i];
1088 if (options[attr]) this[attr] = options[attr];
1089 }
1090 this.options = options;
1091 },
1092
1093 // Ensure that the View has a DOM element to render into.
1094 // If `this.el` is a string, pass it through `$()`, take the first
1095 // matching element, and re-assign it to `el`. Otherwise, create
1096 // an element from the `id`, `className` and `tagName` properties.
1097 _ensureElement: function() {
1098 if (!this.el) {
1099 var attrs = getValue(this, 'attributes') || {};
1100 if (this.id) attrs.id = this.id;
1101 if (this.className) attrs['class'] = this.className;
1102 this.setElement(this.make(this.tagName, attrs), false);
1103 } else {
1104 this.setElement(this.el, false);
1105 }
1106 }
1107
1108 });
1109
1110 // The self-propagating extend function that Backbone classes use.
1111 var extend = function (protoProps, classProps) {
1112 var child = inherits(this, protoProps, classProps);
1113 child.extend = this.extend;
1114 return child;
1115 };
1116
1117 // Set up inheritance for the model, collection, and view.
1118 Backbone.Model.extend = Backbone.Collection.extend =
1119 Backbone.Router.extend = Backbone.View.extend = extend;
1120
1121 // Backbone.sync
1122 // -------------
1123
1124 // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1125 var methodMap = {
1126 'create': 'POST',
1127 'update': 'PUT',
1128 'delete': 'DELETE',
1129 'read': 'GET'
1130 };
1131
1132 // Override this function to change the manner in which Backbone persists
1133 // models to the server. You will be passed the type of request, and the
1134 // model in question. By default, makes a RESTful Ajax request
1135 // to the model's `url()`. Some possible customizations could be:
1136 //
1137 // * Use `setTimeout` to batch rapid-fire updates into a single request.
1138 // * Send up the models as XML instead of JSON.
1139 // * Persist models via WebSockets instead of Ajax.
1140 //
1141 // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1142 // as `POST`, with a `_method` parameter containing the true HTTP method,
1143 // as well as all requests with the body as `application/x-www-form-urlencoded`
1144 // instead of `application/json` with the model in a param named `model`.
1145 // Useful when interfacing with server-side languages like **PHP** that make
1146 // it difficult to read the body of `PUT` requests.
1147 Backbone.sync = function(method, model, options) {
1148 var type = methodMap[method];
1149
1150 // Default JSON-request options.
1151 var params = {type: type, dataType: 'json'};
1152
1153 // Ensure that we have a URL.
1154 if (!options.url) {
1155 params.url = getValue(model, 'url') || urlError();
1156 }
1157
1158 // Ensure that we have the appropriate request data.
1159 if (!options.data && model && (method == 'create' || method == 'update')) {
1160 params.contentType = 'application/json';
1161 params.data = JSON.stringify(model.toJSON());
1162 }
1163
1164 // For older servers, emulate JSON by encoding the request into an HTML-form.
1165 if (Backbone.emulateJSON) {
1166 params.contentType = 'application/x-www-form-urlencoded';
1167 params.data = params.data ? {model: params.data} : {};
1168 }
1169
1170 // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1171 // And an `X-HTTP-Method-Override` header.
1172 if (Backbone.emulateHTTP) {
1173 if (type === 'PUT' || type === 'DELETE') {
1174 if (Backbone.emulateJSON) params.data._method = type;
1175 params.type = 'POST';
1176 params.beforeSend = function(xhr) {
1177 xhr.setRequestHeader('X-HTTP-Method-Override', type);
1178 };
1179 }
1180 }
1181
1182 // Don't process data on a non-GET request.
1183 if (params.type !== 'GET' && !Backbone.emulateJSON) {
1184 params.processData = false;
1185 }
1186
1187 // Make the request, allowing the user to override any Ajax options.
1188 return $.ajax(_.extend(params, options));
1189 };
1190
1191 // Wrap an optional error callback with a fallback error event.
1192 Backbone.wrapError = function(onError, originalModel, options) {
1193 return function(model, resp) {
1194 var resp = model === originalModel ? resp : model;
1195 if (onError) {
1196 onError(model, resp, options);
1197 } else {
1198 originalModel.trigger('error', model, resp, options);
1199 }
1200 };
1201 };
1202
1203 // Helpers
1204 // -------
1205
1206 // Shared empty constructor function to aid in prototype-chain creation.
1207 var ctor = function(){};
1208
1209 // Helper function to correctly set up the prototype chain, for subclasses.
1210 // Similar to `goog.inherits`, but uses a hash of prototype properties and
1211 // class properties to be extended.
1212 var inherits = function(parent, protoProps, staticProps) {
1213 var child;
1214
1215 // The constructor function for the new subclass is either defined by you
1216 // (the "constructor" property in your `extend` definition), or defaulted
1217 // by us to simply call the parent's constructor.
1218 if (protoProps && protoProps.hasOwnProperty('constructor')) {
1219 child = protoProps.constructor;
1220 } else {
1221 child = function(){ parent.apply(this, arguments); };
1222 }
1223
1224 // Inherit class (static) properties from parent.
1225 _.extend(child, parent);
1226
1227 // Set the prototype chain to inherit from `parent`, without calling
1228 // `parent`'s constructor function.
1229 ctor.prototype = parent.prototype;
1230 child.prototype = new ctor();
1231
1232 // Add prototype properties (instance properties) to the subclass,
1233 // if supplied.
1234 if (protoProps) _.extend(child.prototype, protoProps);
1235
1236 // Add static properties to the constructor function, if supplied.
1237 if (staticProps) _.extend(child, staticProps);
1238
1239 // Correctly set child's `prototype.constructor`.
1240 child.prototype.constructor = child;
1241
1242 // Set a convenience property in case the parent's prototype is needed later.
1243 child.__super__ = parent.prototype;
1244
1245 return child;
1246 };
1247
1248 // Helper function to get a value from a Backbone object as a property
1249 // or as a function.
1250 var getValue = function(object, prop) {
1251 if (!(object && object[prop])) return null;
1252 return _.isFunction(object[prop]) ? object[prop]() : object[prop];
1253 };
1254
1255 // Throw an error when a URL is needed, and none is supplied.
1256 var urlError = function() {
1257 throw new Error('A "url" property or function must be specified');
1258 };
1259
1260 }).call(this);