ultimate-base 0.6.2 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,6 +6,8 @@ class Ultimate.Backbone.App
6
6
  name: null
7
7
  warnOnMultibind: true
8
8
  preventMultibind: false
9
+ performanceReport: true
10
+ groupBindingLog: true
9
11
 
10
12
  Models: {}
11
13
  Collections: {}
@@ -24,12 +26,29 @@ class Ultimate.Backbone.App
24
26
  @name = name
25
27
 
26
28
  start: ->
27
- @bindViews()
29
+ @performanceReport = DEBUG_MODE if @performanceReport
30
+ if @performanceReport
31
+ if _.isFunction(performance?.now)
32
+ performanceStart = performance.now()
33
+ else
34
+ cout 'warn', 'performance.now() isnt available'
35
+ @performanceReport = null
36
+ bindedViewsCount = @bindViews().length
37
+ if @performanceReport
38
+ performanceViews = performance.now()
39
+ cout 'info', "Binded #{bindedViewsCount} view#{if bindedViewsCount is 1 then '' else 's'} in #{Math.round((performanceViews - performanceStart) * 1000)}\u00B5s"
28
40
  @bindCustomElements(null, true)
41
+ if @performanceReport
42
+ performanceBinders = performance.now()
43
+ bindersCount = @customElementBinders.length
44
+ cout 'info', "Processed #{bindersCount} custom element binder#{if bindersCount is 1 then '' else 's'} in #{Math.round((performanceBinders - performanceViews) * 1000)}\u00B5s"
29
45
 
30
46
  bindViews: (jRoot = $('html')) ->
47
+ if @groupBindingLog
48
+ console?.groupCollapsed? 'bindViews on', jRoot
31
49
  bindedViews = []
32
- for viewName, viewClass of @Views when viewClass::el
50
+ sortedViews = _.sortBy(_.pairs(@Views), (p) -> p[1].priority)
51
+ for [viewName, viewClass] in sortedViews when viewClass::el
33
52
  #cout 'info', "Try bind #{viewName} [#{viewClass::el}]"
34
53
  jRoot.find(viewClass::el).each (index, el) =>
35
54
  if @canBind(el, viewClass)
@@ -37,6 +56,8 @@ class Ultimate.Backbone.App
37
56
  cout 'info', "Binded view #{viewName}:", view
38
57
  @viewInstances.push view
39
58
  bindedViews.push view
59
+ if @groupBindingLog
60
+ console?.groupEnd?()
40
61
  bindedViews
41
62
 
42
63
  canBind: (element, viewClass) ->
@@ -1,19 +1,37 @@
1
- // Backbone.js 1.0.0
1
+ // Backbone.js 1.1.0
2
2
 
3
- // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc.
3
+ // (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc.
4
+ // (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4
5
  // Backbone may be freely distributed under the MIT license.
5
6
  // For all details and documentation:
6
7
  // http://backbonejs.org
7
8
 
8
- (function(){
9
+ (function(root, factory) {
10
+
11
+ // Set up Backbone appropriately for the environment. Start with AMD.
12
+ if (typeof define === 'function' && define.amd) {
13
+ define(['ultimate/underscore/underscore', 'jquery', 'exports'], function(_, $, exports) {
14
+ // Export global even in AMD case in case this script is loaded with
15
+ // others that may still expect a global Backbone.
16
+ root.Backbone = factory(root, exports, _, $);
17
+ });
18
+
19
+ // Next for Node.js or CommonJS. jQuery may not be needed as a module.
20
+ } else if (typeof exports !== 'undefined') {
21
+ var _ = require('underscore'), $;
22
+ try { $ = require('jquery'); } catch(e) {}
23
+ factory(root, exports, _, $);
24
+
25
+ // Finally, as a browser global.
26
+ } else {
27
+ root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
28
+ }
29
+
30
+ }(this, function(root, Backbone, _, $) {
9
31
 
10
32
  // Initial Setup
11
33
  // -------------
12
34
 
13
- // Save a reference to the global object (`window` in the browser, `exports`
14
- // on the server).
15
- var root = this;
16
-
17
35
  // Save the previous value of the `Backbone` variable, so that it can be
18
36
  // restored later on, if `noConflict` is used.
19
37
  var previousBackbone = root.Backbone;
@@ -24,25 +42,12 @@
24
42
  var slice = array.slice;
25
43
  var splice = array.splice;
26
44
 
27
- // The top-level namespace. All public Backbone classes and modules will
28
- // be attached to this. Exported for both the browser and the server.
29
- var Backbone;
30
- if (typeof exports !== 'undefined') {
31
- Backbone = exports;
32
- } else {
33
- Backbone = root.Backbone = {};
34
- }
35
-
36
45
  // Current version of the library. Keep in sync with `package.json`.
37
- Backbone.VERSION = '1.0.0';
38
-
39
- // Require Underscore, if we're on the server, and it's not already present.
40
- var _ = root._;
41
- if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
46
+ Backbone.VERSION = '1.1.0';
42
47
 
43
48
  // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
44
49
  // the `$` variable.
45
- Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
50
+ Backbone.$ = $;
46
51
 
47
52
  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
48
53
  // to its previous owner. Returns a reference to this Backbone object.
@@ -52,7 +57,7 @@
52
57
  };
53
58
 
54
59
  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
55
- // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
60
+ // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
56
61
  // set a `X-Http-Method-Override` header.
57
62
  Backbone.emulateHTTP = false;
58
63
 
@@ -108,10 +113,9 @@
108
113
  var retain, ev, events, names, i, l, j, k;
109
114
  if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
110
115
  if (!name && !callback && !context) {
111
- this._events = {};
116
+ this._events = void 0;
112
117
  return this;
113
118
  }
114
-
115
119
  names = name ? [name] : _.keys(this._events);
116
120
  for (i = 0, l = names.length; i < l; i++) {
117
121
  name = names[i];
@@ -151,14 +155,15 @@
151
155
  // Tell this object to stop listening to either specific events ... or
152
156
  // to every object it's currently listening to.
153
157
  stopListening: function(obj, name, callback) {
154
- var listeners = this._listeners;
155
- if (!listeners) return this;
156
- var deleteListener = !name && !callback;
157
- if (typeof name === 'object') callback = this;
158
- if (obj) (listeners = {})[obj._listenerId] = obj;
159
- for (var id in listeners) {
160
- listeners[id].off(name, callback, this);
161
- if (deleteListener) delete this._listeners[id];
158
+ var listeningTo = this._listeningTo;
159
+ if (!listeningTo) return this;
160
+ var remove = !name && !callback;
161
+ if (!callback && typeof name === 'object') callback = this;
162
+ if (obj) (listeningTo = {})[obj._listenId] = obj;
163
+ for (var id in listeningTo) {
164
+ obj = listeningTo[id];
165
+ obj.off(name, callback, this);
166
+ if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
162
167
  }
163
168
  return this;
164
169
  }
@@ -204,7 +209,7 @@
204
209
  case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
205
210
  case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
206
211
  case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
207
- default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
212
+ default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
208
213
  }
209
214
  };
210
215
 
@@ -215,10 +220,10 @@
215
220
  // listening to.
216
221
  _.each(listenMethods, function(implementation, method) {
217
222
  Events[method] = function(obj, name, callback) {
218
- var listeners = this._listeners || (this._listeners = {});
219
- var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
220
- listeners[id] = obj;
221
- if (typeof name === 'object') callback = this;
223
+ var listeningTo = this._listeningTo || (this._listeningTo = {});
224
+ var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
225
+ listeningTo[id] = obj;
226
+ if (!callback && typeof name === 'object') callback = this;
222
227
  obj[implementation](name, callback, this);
223
228
  return this;
224
229
  };
@@ -243,24 +248,18 @@
243
248
  // Create a new model with the specified attributes. A client id (`cid`)
244
249
  // is automatically generated and assigned for you.
245
250
  var Model = Backbone.Model = function(attributes, options) {
246
- var defaults;
247
251
  var attrs = attributes || {};
248
252
  options || (options = {});
249
253
  this.cid = _.uniqueId('c');
250
254
  this.attributes = {};
251
- _.extend(this, _.pick(options, modelOptions));
255
+ if (options.collection) this.collection = options.collection;
252
256
  if (options.parse) attrs = this.parse(attrs, options) || {};
253
- if (defaults = _.result(this, 'defaults')) {
254
- attrs = _.defaults({}, attrs, defaults);
255
- }
257
+ attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
256
258
  this.set(attrs, options);
257
259
  this.changed = {};
258
260
  this.initialize.apply(this, arguments);
259
261
  };
260
262
 
261
- // A list of options to be attached directly to the model, if provided.
262
- var modelOptions = ['url', 'urlRoot', 'collection'];
263
-
264
263
  // Attach all inheritable methods to the Model prototype.
265
264
  _.extend(Model.prototype, Events, {
266
265
 
@@ -355,7 +354,7 @@
355
354
 
356
355
  // Trigger all relevant attribute changes.
357
356
  if (!silent) {
358
- if (changes.length) this._pending = true;
357
+ if (changes.length) this._pending = options;
359
358
  for (var i = 0, l = changes.length; i < l; i++) {
360
359
  this.trigger('change:' + changes[i], this, current[changes[i]], options);
361
360
  }
@@ -366,6 +365,7 @@
366
365
  if (changing) return this;
367
366
  if (!silent) {
368
367
  while (this._pending) {
368
+ options = this._pending;
369
369
  this._pending = false;
370
370
  this.trigger('change', this, options);
371
371
  }
@@ -456,13 +456,16 @@
456
456
  (attrs = {})[key] = val;
457
457
  }
458
458
 
459
- // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
460
- if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
461
-
462
459
  options = _.extend({validate: true}, options);
463
460
 
464
- // Do not persist invalid models.
465
- if (!this._validate(attrs, options)) return false;
461
+ // If we're not waiting and attributes exist, save acts as
462
+ // `set(attr).save(null, opts)` with validation. Otherwise, check if
463
+ // the model will be valid when the attributes, if any, are set.
464
+ if (attrs && !options.wait) {
465
+ if (!this.set(attrs, options)) return false;
466
+ } else {
467
+ if (!this._validate(attrs, options)) return false;
468
+ }
466
469
 
467
470
  // Set temporary attributes if `{wait: true}`.
468
471
  if (attrs && options.wait) {
@@ -530,9 +533,12 @@
530
533
  // using Backbone's restful methods, override this to change the endpoint
531
534
  // that will be called.
532
535
  url: function() {
533
- var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
536
+ var base =
537
+ _.result(this, 'urlRoot') ||
538
+ _.result(this.collection, 'url') ||
539
+ urlError();
534
540
  if (this.isNew()) return base;
535
- return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
541
+ return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
536
542
  },
537
543
 
538
544
  // **parse** converts a response into the hash of attributes to be `set` on
@@ -548,7 +554,7 @@
548
554
 
549
555
  // A model is new if it has never been saved to the server, and lacks an id.
550
556
  isNew: function() {
551
- return this.id == null;
557
+ return !this.has(this.idAttribute);
552
558
  },
553
559
 
554
560
  // Check if the model is currently in a valid state.
@@ -563,7 +569,7 @@
563
569
  attrs = _.extend({}, this.attributes, attrs);
564
570
  var error = this.validationError = this.validate(attrs, options) || null;
565
571
  if (!error) return true;
566
- this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
572
+ this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
567
573
  return false;
568
574
  }
569
575
 
@@ -596,7 +602,6 @@
596
602
  // its models in sort order, as they're added and removed.
597
603
  var Collection = Backbone.Collection = function(models, options) {
598
604
  options || (options = {});
599
- if (options.url) this.url = options.url;
600
605
  if (options.model) this.model = options.model;
601
606
  if (options.comparator !== void 0) this.comparator = options.comparator;
602
607
  this._reset();
@@ -606,7 +611,7 @@
606
611
 
607
612
  // Default options for `Collection#set`.
608
613
  var setOptions = {add: true, remove: true, merge: true};
609
- var addOptions = {add: true, merge: false, remove: false};
614
+ var addOptions = {add: true, remove: false};
610
615
 
611
616
  // Define the Collection's inheritable methods.
612
617
  _.extend(Collection.prototype, Events, {
@@ -632,16 +637,17 @@
632
637
 
633
638
  // Add a model, or list of models to the set.
634
639
  add: function(models, options) {
635
- return this.set(models, _.defaults(options || {}, addOptions));
640
+ return this.set(models, _.extend({merge: false}, options, addOptions));
636
641
  },
637
642
 
638
643
  // Remove a model, or a list of models from the set.
639
644
  remove: function(models, options) {
640
- models = _.isArray(models) ? models.slice() : [models];
645
+ var singular = !_.isArray(models);
646
+ models = singular ? [models] : _.clone(models);
641
647
  options || (options = {});
642
648
  var i, l, index, model;
643
649
  for (i = 0, l = models.length; i < l; i++) {
644
- model = this.get(models[i]);
650
+ model = models[i] = this.get(models[i]);
645
651
  if (!model) continue;
646
652
  delete this._byId[model.id];
647
653
  delete this._byId[model.cid];
@@ -652,9 +658,9 @@
652
658
  options.index = index;
653
659
  model.trigger('remove', model, this, options);
654
660
  }
655
- this._removeReference(model);
661
+ this._removeReference(model, options);
656
662
  }
657
- return this;
663
+ return singular ? models[0] : models;
658
664
  },
659
665
 
660
666
  // Update a collection by `set`-ing a new list of models, adding new ones,
@@ -662,43 +668,53 @@
662
668
  // already exist in the collection, as necessary. Similar to **Model#set**,
663
669
  // the core operation for updating the data contained by the collection.
664
670
  set: function(models, options) {
665
- options = _.defaults(options || {}, setOptions);
671
+ options = _.defaults({}, options, setOptions);
666
672
  if (options.parse) models = this.parse(models, options);
667
- if (!_.isArray(models)) models = models ? [models] : [];
668
- var i, l, model, attrs, existing, sort;
673
+ var singular = !_.isArray(models);
674
+ models = singular ? (models ? [models] : []) : _.clone(models);
675
+ var i, l, id, model, attrs, existing, sort;
669
676
  var at = options.at;
677
+ var targetModel = this.model;
670
678
  var sortable = this.comparator && (at == null) && options.sort !== false;
671
679
  var sortAttr = _.isString(this.comparator) ? this.comparator : null;
672
680
  var toAdd = [], toRemove = [], modelMap = {};
681
+ var add = options.add, merge = options.merge, remove = options.remove;
682
+ var order = !sortable && add && remove ? [] : false;
673
683
 
674
684
  // Turn bare objects into model references, and prevent invalid models
675
685
  // from being added.
676
686
  for (i = 0, l = models.length; i < l; i++) {
677
- if (!(model = this._prepareModel(models[i], options))) continue;
687
+ attrs = models[i] || {};
688
+ if (attrs instanceof Model) {
689
+ id = model = attrs;
690
+ } else {
691
+ id = attrs[targetModel.prototype.idAttribute || 'id'];
692
+ }
678
693
 
679
694
  // If a duplicate is found, prevent it from being added and
680
695
  // optionally merge it into the existing model.
681
- if (existing = this.get(model)) {
682
- if (options.remove) modelMap[existing.cid] = true;
683
- if (options.merge) {
684
- existing.set(model.attributes, options);
696
+ if (existing = this.get(id)) {
697
+ if (remove) modelMap[existing.cid] = true;
698
+ if (merge) {
699
+ attrs = attrs === model ? model.attributes : attrs;
700
+ if (options.parse) attrs = existing.parse(attrs, options);
701
+ existing.set(attrs, options);
685
702
  if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
686
703
  }
704
+ models[i] = existing;
687
705
 
688
- // This is a new model, push it to the `toAdd` list.
689
- } else if (options.add) {
706
+ // If this is a new, valid model, push it to the `toAdd` list.
707
+ } else if (add) {
708
+ model = models[i] = this._prepareModel(attrs, options);
709
+ if (!model) continue;
690
710
  toAdd.push(model);
691
-
692
- // Listen to added models' events, and index models for lookup by
693
- // `id` and by `cid`.
694
- model.on('all', this._onModelEvent, this);
695
- this._byId[model.cid] = model;
696
- if (model.id != null) this._byId[model.id] = model;
711
+ this._addReference(model, options);
697
712
  }
713
+ if (order) order.push(existing || model);
698
714
  }
699
715
 
700
716
  // Remove nonexistent models if appropriate.
701
- if (options.remove) {
717
+ if (remove) {
702
718
  for (i = 0, l = this.length; i < l; ++i) {
703
719
  if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
704
720
  }
@@ -706,29 +722,35 @@
706
722
  }
707
723
 
708
724
  // See if sorting is needed, update `length` and splice in new models.
709
- if (toAdd.length) {
725
+ if (toAdd.length || (order && order.length)) {
710
726
  if (sortable) sort = true;
711
727
  this.length += toAdd.length;
712
728
  if (at != null) {
713
- splice.apply(this.models, [at, 0].concat(toAdd));
729
+ for (i = 0, l = toAdd.length; i < l; i++) {
730
+ this.models.splice(at + i, 0, toAdd[i]);
731
+ }
714
732
  } else {
715
- push.apply(this.models, toAdd);
733
+ if (order) this.models.length = 0;
734
+ var orderedModels = order || toAdd;
735
+ for (i = 0, l = orderedModels.length; i < l; i++) {
736
+ this.models.push(orderedModels[i]);
737
+ }
716
738
  }
717
739
  }
718
740
 
719
741
  // Silently sort the collection if appropriate.
720
742
  if (sort) this.sort({silent: true});
721
743
 
722
- if (options.silent) return this;
723
-
724
- // Trigger `add` events.
725
- for (i = 0, l = toAdd.length; i < l; i++) {
726
- (model = toAdd[i]).trigger('add', model, this, options);
744
+ // Unless silenced, it's time to fire all appropriate add/sort events.
745
+ if (!options.silent) {
746
+ for (i = 0, l = toAdd.length; i < l; i++) {
747
+ (model = toAdd[i]).trigger('add', model, this, options);
748
+ }
749
+ if (sort || (order && order.length)) this.trigger('sort', this, options);
727
750
  }
728
751
 
729
- // Trigger `sort` if the collection was sorted.
730
- if (sort) this.trigger('sort', this, options);
731
- return this;
752
+ // Return the added (or merged) model (or models).
753
+ return singular ? models[0] : models;
732
754
  },
733
755
 
734
756
  // When you have more items than you want to add or remove individually,
@@ -738,20 +760,18 @@
738
760
  reset: function(models, options) {
739
761
  options || (options = {});
740
762
  for (var i = 0, l = this.models.length; i < l; i++) {
741
- this._removeReference(this.models[i]);
763
+ this._removeReference(this.models[i], options);
742
764
  }
743
765
  options.previousModels = this.models;
744
766
  this._reset();
745
- this.add(models, _.extend({silent: true}, options));
767
+ models = this.add(models, _.extend({silent: true}, options));
746
768
  if (!options.silent) this.trigger('reset', this, options);
747
- return this;
769
+ return models;
748
770
  },
749
771
 
750
772
  // Add a model to the end of the collection.
751
773
  push: function(model, options) {
752
- model = this._prepareModel(model, options);
753
- this.add(model, _.extend({at: this.length}, options));
754
- return model;
774
+ return this.add(model, _.extend({at: this.length}, options));
755
775
  },
756
776
 
757
777
  // Remove a model from the end of the collection.
@@ -763,9 +783,7 @@
763
783
 
764
784
  // Add a model to the beginning of the collection.
765
785
  unshift: function(model, options) {
766
- model = this._prepareModel(model, options);
767
- this.add(model, _.extend({at: 0}, options));
768
- return model;
786
+ return this.add(model, _.extend({at: 0}, options));
769
787
  },
770
788
 
771
789
  // Remove a model from the beginning of the collection.
@@ -776,14 +794,14 @@
776
794
  },
777
795
 
778
796
  // Slice out a sub-array of models from the collection.
779
- slice: function(begin, end) {
780
- return this.models.slice(begin, end);
797
+ slice: function() {
798
+ return slice.apply(this.models, arguments);
781
799
  },
782
800
 
783
801
  // Get a model from the set by id.
784
802
  get: function(obj) {
785
803
  if (obj == null) return void 0;
786
- return this._byId[obj.id != null ? obj.id : obj.cid || obj];
804
+ return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
787
805
  },
788
806
 
789
807
  // Get the model at the given index.
@@ -827,16 +845,6 @@
827
845
  return this;
828
846
  },
829
847
 
830
- // Figure out the smallest index at which a model should be inserted so as
831
- // to maintain order.
832
- sortedIndex: function(model, value, context) {
833
- value || (value = this.comparator);
834
- var iterator = _.isFunction(value) ? value : function(model) {
835
- return model.get(value);
836
- };
837
- return _.sortedIndex(this.models, model, iterator, context);
838
- },
839
-
840
848
  // Pluck an attribute from each model in the collection.
841
849
  pluck: function(attr) {
842
850
  return _.invoke(this.models, 'get', attr);
@@ -869,7 +877,7 @@
869
877
  if (!options.wait) this.add(model, options);
870
878
  var collection = this;
871
879
  var success = options.success;
872
- options.success = function(resp) {
880
+ options.success = function(model, resp) {
873
881
  if (options.wait) collection.add(model, options);
874
882
  if (success) success(model, resp, options);
875
883
  };
@@ -899,22 +907,25 @@
899
907
  // Prepare a hash of attributes (or other model) to be added to this
900
908
  // collection.
901
909
  _prepareModel: function(attrs, options) {
902
- if (attrs instanceof Model) {
903
- if (!attrs.collection) attrs.collection = this;
904
- return attrs;
905
- }
906
- options || (options = {});
910
+ if (attrs instanceof Model) return attrs;
911
+ options = options ? _.clone(options) : {};
907
912
  options.collection = this;
908
913
  var model = new this.model(attrs, options);
909
- if (!model._validate(attrs, options)) {
910
- this.trigger('invalid', this, attrs, options);
911
- return false;
912
- }
913
- return model;
914
+ if (!model.validationError) return model;
915
+ this.trigger('invalid', this, model.validationError, options);
916
+ return false;
917
+ },
918
+
919
+ // Internal method to create a model's ties to a collection.
920
+ _addReference: function(model, options) {
921
+ this._byId[model.cid] = model;
922
+ if (model.id != null) this._byId[model.id] = model;
923
+ if (!model.collection) model.collection = this;
924
+ model.on('all', this._onModelEvent, this);
914
925
  },
915
926
 
916
927
  // Internal method to sever a model's ties to a collection.
917
- _removeReference: function(model) {
928
+ _removeReference: function(model, options) {
918
929
  if (this === model.collection) delete model.collection;
919
930
  model.off('all', this._onModelEvent, this);
920
931
  },
@@ -942,8 +953,8 @@
942
953
  'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
943
954
  'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
944
955
  'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
945
- 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
946
- 'isEmpty', 'chain'];
956
+ 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
957
+ 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
947
958
 
948
959
  // Mix in each Underscore method as a proxy to `Collection#models`.
949
960
  _.each(methods, function(method) {
@@ -955,7 +966,7 @@
955
966
  });
956
967
 
957
968
  // Underscore methods that take a property name as an argument.
958
- var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
969
+ var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
959
970
 
960
971
  // Use attributes instead of properties.
961
972
  _.each(attributeMethods, function(method) {
@@ -982,7 +993,8 @@
982
993
  // if an existing element is not provided...
983
994
  var View = Backbone.View = function(options) {
984
995
  this.cid = _.uniqueId('view');
985
- this._configure(options || {});
996
+ options || (options = {});
997
+ _.extend(this, _.pick(options, viewOptions));
986
998
  this._ensureElement();
987
999
  this.initialize.apply(this, arguments);
988
1000
  this.delegateEvents();
@@ -1001,7 +1013,7 @@
1001
1013
  tagName: 'div',
1002
1014
 
1003
1015
  // jQuery delegate for element lookup, scoped to DOM elements within the
1004
- // current view. This should be prefered to global lookups where possible.
1016
+ // current view. This should be preferred to global lookups where possible.
1005
1017
  $: function(selector) {
1006
1018
  return this.$el.find(selector);
1007
1019
  },
@@ -1041,7 +1053,7 @@
1041
1053
  //
1042
1054
  // {
1043
1055
  // 'mousedown .title': 'edit',
1044
- // 'click .button': 'save'
1056
+ // 'click .button': 'save',
1045
1057
  // 'click .open': function(e) { ... }
1046
1058
  // }
1047
1059
  //
@@ -1079,16 +1091,6 @@
1079
1091
  return this;
1080
1092
  },
1081
1093
 
1082
- // Performs the initial configuration of a View with a set of options.
1083
- // Keys with special meaning *(e.g. model, collection, id, className)* are
1084
- // attached directly to the view. See `viewOptions` for an exhaustive
1085
- // list.
1086
- _configure: function(options) {
1087
- if (this.options) options = _.extend({}, _.result(this, 'options'), options);
1088
- _.extend(this, _.pick(options, viewOptions));
1089
- this.options = options;
1090
- },
1091
-
1092
1094
  // Ensure that the View has a DOM element to render into.
1093
1095
  // If `this.el` is a string, pass it through `$()`, take the first
1094
1096
  // matching element, and re-assign it to `el`. Otherwise, create
@@ -1174,8 +1176,7 @@
1174
1176
  // If we're sending a `PATCH` request, and we're in an old Internet Explorer
1175
1177
  // that still has ActiveX enabled by default, override jQuery to use that
1176
1178
  // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
1177
- if (params.type === 'PATCH' && window.ActiveXObject &&
1178
- !(window.external && window.external.msActiveXFilteringEnabled)) {
1179
+ if (params.type === 'PATCH' && noXhrPatch) {
1179
1180
  params.xhr = function() {
1180
1181
  return new ActiveXObject("Microsoft.XMLHTTP");
1181
1182
  };
@@ -1187,6 +1188,10 @@
1187
1188
  return xhr;
1188
1189
  };
1189
1190
 
1191
+ var noXhrPatch =
1192
+ typeof window !== 'undefined' && !!window.ActiveXObject &&
1193
+ !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
1194
+
1190
1195
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1191
1196
  var methodMap = {
1192
1197
  'create': 'POST',
@@ -1244,7 +1249,7 @@
1244
1249
  var router = this;
1245
1250
  Backbone.history.route(route, function(fragment) {
1246
1251
  var args = router._extractParameters(route, fragment);
1247
- callback && callback.apply(router, args);
1252
+ router.execute(callback, args);
1248
1253
  router.trigger.apply(router, ['route:' + name].concat(args));
1249
1254
  router.trigger('route', name, args);
1250
1255
  Backbone.history.trigger('route', router, name, args);
@@ -1252,6 +1257,12 @@
1252
1257
  return this;
1253
1258
  },
1254
1259
 
1260
+ // Execute a route handler with the provided parameters. This is an
1261
+ // excellent place to do pre-route setup or post-route cleanup.
1262
+ execute: function(callback, args) {
1263
+ if (callback) callback.apply(this, args);
1264
+ },
1265
+
1255
1266
  // Simple proxy to `Backbone.history` to save a fragment into the history.
1256
1267
  navigate: function(fragment, options) {
1257
1268
  Backbone.history.navigate(fragment, options);
@@ -1275,11 +1286,11 @@
1275
1286
  _routeToRegExp: function(route) {
1276
1287
  route = route.replace(escapeRegExp, '\\$&')
1277
1288
  .replace(optionalParam, '(?:$1)?')
1278
- .replace(namedParam, function(match, optional){
1279
- return optional ? match : '([^\/]+)';
1289
+ .replace(namedParam, function(match, optional) {
1290
+ return optional ? match : '([^/?]+)';
1280
1291
  })
1281
- .replace(splatParam, '(.*?)');
1282
- return new RegExp('^' + route + '$');
1292
+ .replace(splatParam, '([^?]*?)');
1293
+ return new RegExp('^' + route + '(?:\\?(.*))?$');
1283
1294
  },
1284
1295
 
1285
1296
  // Given a route, and a URL fragment that it matches, return the array of
@@ -1287,7 +1298,9 @@
1287
1298
  // treated as `null` to normalize cross-browser behavior.
1288
1299
  _extractParameters: function(route, fragment) {
1289
1300
  var params = route.exec(fragment).slice(1);
1290
- return _.map(params, function(param) {
1301
+ return _.map(params, function(param, i) {
1302
+ // Don't decode the search params.
1303
+ if (i === params.length - 1) return param || null;
1291
1304
  return param ? decodeURIComponent(param) : null;
1292
1305
  });
1293
1306
  }
@@ -1325,6 +1338,9 @@
1325
1338
  // Cached regex for removing a trailing slash.
1326
1339
  var trailingSlash = /\/$/;
1327
1340
 
1341
+ // Cached regex for stripping urls of hash.
1342
+ var pathStripper = /#.*$/;
1343
+
1328
1344
  // Has the history handling already been started?
1329
1345
  History.started = false;
1330
1346
 
@@ -1335,6 +1351,11 @@
1335
1351
  // twenty times a second.
1336
1352
  interval: 50,
1337
1353
 
1354
+ // Are we at the app root?
1355
+ atRoot: function() {
1356
+ return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
1357
+ },
1358
+
1338
1359
  // Gets the true hash value. Cannot use location.hash directly due to bug
1339
1360
  // in Firefox where location.hash will always be decoded.
1340
1361
  getHash: function(window) {
@@ -1347,9 +1368,9 @@
1347
1368
  getFragment: function(fragment, forcePushState) {
1348
1369
  if (fragment == null) {
1349
1370
  if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1350
- fragment = this.location.pathname;
1371
+ fragment = this.location.pathname + this.location.search;
1351
1372
  var root = this.root.replace(trailingSlash, '');
1352
- if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
1373
+ if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
1353
1374
  } else {
1354
1375
  fragment = this.getHash();
1355
1376
  }
@@ -1365,7 +1386,7 @@
1365
1386
 
1366
1387
  // Figure out the initial configuration. Do we need an iframe?
1367
1388
  // Is pushState desired ... is it available?
1368
- this.options = _.extend({}, {root: '/'}, this.options, options);
1389
+ this.options = _.extend({root: '/'}, this.options, options);
1369
1390
  this.root = this.options.root;
1370
1391
  this._wantsHashChange = this.options.hashChange !== false;
1371
1392
  this._wantsPushState = !!this.options.pushState;
@@ -1378,7 +1399,8 @@
1378
1399
  this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1379
1400
 
1380
1401
  if (oldIE && this._wantsHashChange) {
1381
- this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
1402
+ var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
1403
+ this.iframe = frame.hide().appendTo('body')[0].contentWindow;
1382
1404
  this.navigate(fragment);
1383
1405
  }
1384
1406
 
@@ -1396,21 +1418,26 @@
1396
1418
  // opened by a non-pushState browser.
1397
1419
  this.fragment = fragment;
1398
1420
  var loc = this.location;
1399
- var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1400
-
1401
- // If we've started off with a route from a `pushState`-enabled browser,
1402
- // but we're currently in a browser that doesn't support it...
1403
- if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1404
- this.fragment = this.getFragment(null, true);
1405
- this.location.replace(this.root + this.location.search + '#' + this.fragment);
1406
- // Return immediately as browser will do redirect to new url
1407
- return true;
1408
1421
 
1409
- // Or if we've started out with a hash-based route, but we're currently
1410
- // in a browser where it could be `pushState`-based instead...
1411
- } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1412
- this.fragment = this.getHash().replace(routeStripper, '');
1413
- this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1422
+ // Transition from hashChange to pushState or vice versa if both are
1423
+ // requested.
1424
+ if (this._wantsHashChange && this._wantsPushState) {
1425
+
1426
+ // If we've started off with a route from a `pushState`-enabled
1427
+ // browser, but we're currently in a browser that doesn't support it...
1428
+ if (!this._hasPushState && !this.atRoot()) {
1429
+ this.fragment = this.getFragment(null, true);
1430
+ this.location.replace(this.root + '#' + this.fragment);
1431
+ // Return immediately as browser will do redirect to new url
1432
+ return true;
1433
+
1434
+ // Or if we've started out with a hash-based route, but we're currently
1435
+ // in a browser where it could be `pushState`-based instead...
1436
+ } else if (this._hasPushState && this.atRoot() && loc.hash) {
1437
+ this.fragment = this.getHash().replace(routeStripper, '');
1438
+ this.history.replaceState({}, document.title, this.root + this.fragment);
1439
+ }
1440
+
1414
1441
  }
1415
1442
 
1416
1443
  if (!this.options.silent) return this.loadUrl();
@@ -1439,21 +1466,20 @@
1439
1466
  }
1440
1467
  if (current === this.fragment) return false;
1441
1468
  if (this.iframe) this.navigate(current);
1442
- this.loadUrl() || this.loadUrl(this.getHash());
1469
+ this.loadUrl();
1443
1470
  },
1444
1471
 
1445
1472
  // Attempt to load the current URL fragment. If a route succeeds with a
1446
1473
  // match, returns `true`. If no defined routes matches the fragment,
1447
1474
  // returns `false`.
1448
- loadUrl: function(fragmentOverride) {
1449
- var fragment = this.fragment = this.getFragment(fragmentOverride);
1450
- var matched = _.any(this.handlers, function(handler) {
1475
+ loadUrl: function(fragment) {
1476
+ fragment = this.fragment = this.getFragment(fragment);
1477
+ return _.any(this.handlers, function(handler) {
1451
1478
  if (handler.route.test(fragment)) {
1452
1479
  handler.callback(fragment);
1453
1480
  return true;
1454
1481
  }
1455
1482
  });
1456
- return matched;
1457
1483
  },
1458
1484
 
1459
1485
  // Save a fragment into the hash history, or replace the URL state if the
@@ -1465,11 +1491,18 @@
1465
1491
  // you wish to modify the current URL without adding an entry to the history.
1466
1492
  navigate: function(fragment, options) {
1467
1493
  if (!History.started) return false;
1468
- if (!options || options === true) options = {trigger: options};
1469
- fragment = this.getFragment(fragment || '');
1494
+ if (!options || options === true) options = {trigger: !!options};
1495
+
1496
+ var url = this.root + (fragment = this.getFragment(fragment || ''));
1497
+
1498
+ // Strip the hash for matching.
1499
+ fragment = fragment.replace(pathStripper, '');
1500
+
1470
1501
  if (this.fragment === fragment) return;
1471
1502
  this.fragment = fragment;
1472
- var url = this.root + fragment;
1503
+
1504
+ // Don't include a trailing slash on the root.
1505
+ if (fragment === '' && url !== '/') url = url.slice(0, -1);
1473
1506
 
1474
1507
  // If pushState is available, we use it to set the fragment as a real URL.
1475
1508
  if (this._hasPushState) {
@@ -1492,7 +1525,7 @@
1492
1525
  } else {
1493
1526
  return this.location.assign(url);
1494
1527
  }
1495
- if (options.trigger) this.loadUrl(fragment);
1528
+ if (options.trigger) return this.loadUrl(fragment);
1496
1529
  },
1497
1530
 
1498
1531
  // Update the hash location, either replacing the current entry, or adding
@@ -1560,7 +1593,7 @@
1560
1593
  };
1561
1594
 
1562
1595
  // Wrap an optional error callback with a fallback error event.
1563
- var wrapError = function (model, options) {
1596
+ var wrapError = function(model, options) {
1564
1597
  var error = options.error;
1565
1598
  options.error = function(resp) {
1566
1599
  if (error) error(model, resp, options);
@@ -1568,4 +1601,6 @@
1568
1601
  };
1569
1602
  };
1570
1603
 
1571
- }).call(this);
1604
+ return Backbone;
1605
+
1606
+ }));