marionette.modal 1.0.0.6 → 1.0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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(['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
  }
@@ -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) {
@@ -548,7 +551,7 @@
548
551
 
549
552
  // A model is new if it has never been saved to the server, and lacks an id.
550
553
  isNew: function() {
551
- return this.id == null;
554
+ return !this.has(this.idAttribute);
552
555
  },
553
556
 
554
557
  // Check if the model is currently in a valid state.
@@ -563,7 +566,7 @@
563
566
  attrs = _.extend({}, this.attributes, attrs);
564
567
  var error = this.validationError = this.validate(attrs, options) || null;
565
568
  if (!error) return true;
566
- this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
569
+ this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
567
570
  return false;
568
571
  }
569
572
 
@@ -596,7 +599,6 @@
596
599
  // its models in sort order, as they're added and removed.
597
600
  var Collection = Backbone.Collection = function(models, options) {
598
601
  options || (options = {});
599
- if (options.url) this.url = options.url;
600
602
  if (options.model) this.model = options.model;
601
603
  if (options.comparator !== void 0) this.comparator = options.comparator;
602
604
  this._reset();
@@ -606,7 +608,7 @@
606
608
 
607
609
  // Default options for `Collection#set`.
608
610
  var setOptions = {add: true, remove: true, merge: true};
609
- var addOptions = {add: true, merge: false, remove: false};
611
+ var addOptions = {add: true, remove: false};
610
612
 
611
613
  // Define the Collection's inheritable methods.
612
614
  _.extend(Collection.prototype, Events, {
@@ -632,19 +634,18 @@
632
634
 
633
635
  // Add a model, or list of models to the set.
634
636
  add: function(models, options) {
635
- return this.set(models, _.defaults(options || {}, addOptions));
637
+ return this.set(models, _.extend({merge: false}, options, addOptions));
636
638
  },
637
639
 
638
640
  // Remove a model, or a list of models from the set.
639
641
  remove: function(models, options) {
640
- models = _.isArray(models) ? models.slice() : [models];
642
+ var singular = !_.isArray(models);
643
+ models = singular ? [models] : _.clone(models);
641
644
  options || (options = {});
642
645
  var i, l, index, model;
643
646
  for (i = 0, l = models.length; i < l; i++) {
644
- model = this.get(models[i]);
647
+ model = models[i] = this.get(models[i]);
645
648
  if (!model) continue;
646
- delete this._byId[model.id];
647
- delete this._byId[model.cid];
648
649
  index = this.indexOf(model);
649
650
  this.models.splice(index, 1);
650
651
  this.length--;
@@ -652,9 +653,9 @@
652
653
  options.index = index;
653
654
  model.trigger('remove', model, this, options);
654
655
  }
655
- this._removeReference(model);
656
+ this._removeReference(model, options);
656
657
  }
657
- return this;
658
+ return singular ? models[0] : models;
658
659
  },
659
660
 
660
661
  // Update a collection by `set`-ing a new list of models, adding new ones,
@@ -662,43 +663,53 @@
662
663
  // already exist in the collection, as necessary. Similar to **Model#set**,
663
664
  // the core operation for updating the data contained by the collection.
664
665
  set: function(models, options) {
665
- options = _.defaults(options || {}, setOptions);
666
+ options = _.defaults({}, options, setOptions);
666
667
  if (options.parse) models = this.parse(models, options);
667
- if (!_.isArray(models)) models = models ? [models] : [];
668
- var i, l, model, attrs, existing, sort;
668
+ var singular = !_.isArray(models);
669
+ models = singular ? (models ? [models] : []) : _.clone(models);
670
+ var i, l, id, model, attrs, existing, sort;
669
671
  var at = options.at;
672
+ var targetModel = this.model;
670
673
  var sortable = this.comparator && (at == null) && options.sort !== false;
671
674
  var sortAttr = _.isString(this.comparator) ? this.comparator : null;
672
675
  var toAdd = [], toRemove = [], modelMap = {};
676
+ var add = options.add, merge = options.merge, remove = options.remove;
677
+ var order = !sortable && add && remove ? [] : false;
673
678
 
674
679
  // Turn bare objects into model references, and prevent invalid models
675
680
  // from being added.
676
681
  for (i = 0, l = models.length; i < l; i++) {
677
- if (!(model = this._prepareModel(models[i], options))) continue;
682
+ attrs = models[i] || {};
683
+ if (attrs instanceof Model) {
684
+ id = model = attrs;
685
+ } else {
686
+ id = attrs[targetModel.prototype.idAttribute || 'id'];
687
+ }
678
688
 
679
689
  // If a duplicate is found, prevent it from being added and
680
690
  // 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);
691
+ if (existing = this.get(id)) {
692
+ if (remove) modelMap[existing.cid] = true;
693
+ if (merge) {
694
+ attrs = attrs === model ? model.attributes : attrs;
695
+ if (options.parse) attrs = existing.parse(attrs, options);
696
+ existing.set(attrs, options);
685
697
  if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
686
698
  }
699
+ models[i] = existing;
687
700
 
688
- // This is a new model, push it to the `toAdd` list.
689
- } else if (options.add) {
701
+ // If this is a new, valid model, push it to the `toAdd` list.
702
+ } else if (add) {
703
+ model = models[i] = this._prepareModel(attrs, options);
704
+ if (!model) continue;
690
705
  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;
706
+ this._addReference(model, options);
697
707
  }
708
+ if (order) order.push(existing || model);
698
709
  }
699
710
 
700
711
  // Remove nonexistent models if appropriate.
701
- if (options.remove) {
712
+ if (remove) {
702
713
  for (i = 0, l = this.length; i < l; ++i) {
703
714
  if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
704
715
  }
@@ -706,29 +717,35 @@
706
717
  }
707
718
 
708
719
  // See if sorting is needed, update `length` and splice in new models.
709
- if (toAdd.length) {
720
+ if (toAdd.length || (order && order.length)) {
710
721
  if (sortable) sort = true;
711
722
  this.length += toAdd.length;
712
723
  if (at != null) {
713
- splice.apply(this.models, [at, 0].concat(toAdd));
724
+ for (i = 0, l = toAdd.length; i < l; i++) {
725
+ this.models.splice(at + i, 0, toAdd[i]);
726
+ }
714
727
  } else {
715
- push.apply(this.models, toAdd);
728
+ if (order) this.models.length = 0;
729
+ var orderedModels = order || toAdd;
730
+ for (i = 0, l = orderedModels.length; i < l; i++) {
731
+ this.models.push(orderedModels[i]);
732
+ }
716
733
  }
717
734
  }
718
735
 
719
736
  // Silently sort the collection if appropriate.
720
737
  if (sort) this.sort({silent: true});
721
738
 
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);
739
+ // Unless silenced, it's time to fire all appropriate add/sort events.
740
+ if (!options.silent) {
741
+ for (i = 0, l = toAdd.length; i < l; i++) {
742
+ (model = toAdd[i]).trigger('add', model, this, options);
743
+ }
744
+ if (sort || (order && order.length)) this.trigger('sort', this, options);
727
745
  }
728
746
 
729
- // Trigger `sort` if the collection was sorted.
730
- if (sort) this.trigger('sort', this, options);
731
- return this;
747
+ // Return the added (or merged) model (or models).
748
+ return singular ? models[0] : models;
732
749
  },
733
750
 
734
751
  // When you have more items than you want to add or remove individually,
@@ -738,20 +755,18 @@
738
755
  reset: function(models, options) {
739
756
  options || (options = {});
740
757
  for (var i = 0, l = this.models.length; i < l; i++) {
741
- this._removeReference(this.models[i]);
758
+ this._removeReference(this.models[i], options);
742
759
  }
743
760
  options.previousModels = this.models;
744
761
  this._reset();
745
- this.add(models, _.extend({silent: true}, options));
762
+ models = this.add(models, _.extend({silent: true}, options));
746
763
  if (!options.silent) this.trigger('reset', this, options);
747
- return this;
764
+ return models;
748
765
  },
749
766
 
750
767
  // Add a model to the end of the collection.
751
768
  push: function(model, options) {
752
- model = this._prepareModel(model, options);
753
- this.add(model, _.extend({at: this.length}, options));
754
- return model;
769
+ return this.add(model, _.extend({at: this.length}, options));
755
770
  },
756
771
 
757
772
  // Remove a model from the end of the collection.
@@ -763,9 +778,7 @@
763
778
 
764
779
  // Add a model to the beginning of the collection.
765
780
  unshift: function(model, options) {
766
- model = this._prepareModel(model, options);
767
- this.add(model, _.extend({at: 0}, options));
768
- return model;
781
+ return this.add(model, _.extend({at: 0}, options));
769
782
  },
770
783
 
771
784
  // Remove a model from the beginning of the collection.
@@ -776,14 +789,14 @@
776
789
  },
777
790
 
778
791
  // Slice out a sub-array of models from the collection.
779
- slice: function(begin, end) {
780
- return this.models.slice(begin, end);
792
+ slice: function() {
793
+ return slice.apply(this.models, arguments);
781
794
  },
782
795
 
783
796
  // Get a model from the set by id.
784
797
  get: function(obj) {
785
798
  if (obj == null) return void 0;
786
- return this._byId[obj.id != null ? obj.id : obj.cid || obj];
799
+ return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
787
800
  },
788
801
 
789
802
  // Get the model at the given index.
@@ -827,16 +840,6 @@
827
840
  return this;
828
841
  },
829
842
 
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
843
  // Pluck an attribute from each model in the collection.
841
844
  pluck: function(attr) {
842
845
  return _.invoke(this.models, 'get', attr);
@@ -869,7 +872,7 @@
869
872
  if (!options.wait) this.add(model, options);
870
873
  var collection = this;
871
874
  var success = options.success;
872
- options.success = function(resp) {
875
+ options.success = function(model, resp) {
873
876
  if (options.wait) collection.add(model, options);
874
877
  if (success) success(model, resp, options);
875
878
  };
@@ -899,22 +902,27 @@
899
902
  // Prepare a hash of attributes (or other model) to be added to this
900
903
  // collection.
901
904
  _prepareModel: function(attrs, options) {
902
- if (attrs instanceof Model) {
903
- if (!attrs.collection) attrs.collection = this;
904
- return attrs;
905
- }
906
- options || (options = {});
905
+ if (attrs instanceof Model) return attrs;
906
+ options = options ? _.clone(options) : {};
907
907
  options.collection = this;
908
908
  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;
909
+ if (!model.validationError) return model;
910
+ this.trigger('invalid', this, model.validationError, options);
911
+ return false;
912
+ },
913
+
914
+ // Internal method to create a model's ties to a collection.
915
+ _addReference: function(model, options) {
916
+ this._byId[model.cid] = model;
917
+ if (model.id != null) this._byId[model.id] = model;
918
+ if (!model.collection) model.collection = this;
919
+ model.on('all', this._onModelEvent, this);
914
920
  },
915
921
 
916
922
  // Internal method to sever a model's ties to a collection.
917
- _removeReference: function(model) {
923
+ _removeReference: function(model, options) {
924
+ delete this._byId[model.id];
925
+ delete this._byId[model.cid];
918
926
  if (this === model.collection) delete model.collection;
919
927
  model.off('all', this._onModelEvent, this);
920
928
  },
@@ -942,8 +950,8 @@
942
950
  'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
943
951
  'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
944
952
  'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
945
- 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
946
- 'isEmpty', 'chain'];
953
+ 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
954
+ 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
947
955
 
948
956
  // Mix in each Underscore method as a proxy to `Collection#models`.
949
957
  _.each(methods, function(method) {
@@ -955,7 +963,7 @@
955
963
  });
956
964
 
957
965
  // Underscore methods that take a property name as an argument.
958
- var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
966
+ var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
959
967
 
960
968
  // Use attributes instead of properties.
961
969
  _.each(attributeMethods, function(method) {
@@ -982,7 +990,8 @@
982
990
  // if an existing element is not provided...
983
991
  var View = Backbone.View = function(options) {
984
992
  this.cid = _.uniqueId('view');
985
- this._configure(options || {});
993
+ options || (options = {});
994
+ _.extend(this, _.pick(options, viewOptions));
986
995
  this._ensureElement();
987
996
  this.initialize.apply(this, arguments);
988
997
  this.delegateEvents();
@@ -1001,7 +1010,7 @@
1001
1010
  tagName: 'div',
1002
1011
 
1003
1012
  // jQuery delegate for element lookup, scoped to DOM elements within the
1004
- // current view. This should be prefered to global lookups where possible.
1013
+ // current view. This should be preferred to global lookups where possible.
1005
1014
  $: function(selector) {
1006
1015
  return this.$el.find(selector);
1007
1016
  },
@@ -1041,7 +1050,7 @@
1041
1050
  //
1042
1051
  // {
1043
1052
  // 'mousedown .title': 'edit',
1044
- // 'click .button': 'save'
1053
+ // 'click .button': 'save',
1045
1054
  // 'click .open': function(e) { ... }
1046
1055
  // }
1047
1056
  //
@@ -1079,16 +1088,6 @@
1079
1088
  return this;
1080
1089
  },
1081
1090
 
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
1091
  // Ensure that the View has a DOM element to render into.
1093
1092
  // If `this.el` is a string, pass it through `$()`, take the first
1094
1093
  // matching element, and re-assign it to `el`. Otherwise, create
@@ -1174,8 +1173,7 @@
1174
1173
  // If we're sending a `PATCH` request, and we're in an old Internet Explorer
1175
1174
  // that still has ActiveX enabled by default, override jQuery to use that
1176
1175
  // 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)) {
1176
+ if (params.type === 'PATCH' && noXhrPatch) {
1179
1177
  params.xhr = function() {
1180
1178
  return new ActiveXObject("Microsoft.XMLHTTP");
1181
1179
  };
@@ -1187,6 +1185,10 @@
1187
1185
  return xhr;
1188
1186
  };
1189
1187
 
1188
+ var noXhrPatch =
1189
+ typeof window !== 'undefined' && !!window.ActiveXObject &&
1190
+ !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
1191
+
1190
1192
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1191
1193
  var methodMap = {
1192
1194
  'create': 'POST',
@@ -1275,7 +1277,7 @@
1275
1277
  _routeToRegExp: function(route) {
1276
1278
  route = route.replace(escapeRegExp, '\\$&')
1277
1279
  .replace(optionalParam, '(?:$1)?')
1278
- .replace(namedParam, function(match, optional){
1280
+ .replace(namedParam, function(match, optional) {
1279
1281
  return optional ? match : '([^\/]+)';
1280
1282
  })
1281
1283
  .replace(splatParam, '(.*?)');
@@ -1325,6 +1327,9 @@
1325
1327
  // Cached regex for removing a trailing slash.
1326
1328
  var trailingSlash = /\/$/;
1327
1329
 
1330
+ // Cached regex for stripping urls of hash and query.
1331
+ var pathStripper = /[?#].*$/;
1332
+
1328
1333
  // Has the history handling already been started?
1329
1334
  History.started = false;
1330
1335
 
@@ -1349,7 +1354,7 @@
1349
1354
  if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1350
1355
  fragment = this.location.pathname;
1351
1356
  var root = this.root.replace(trailingSlash, '');
1352
- if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
1357
+ if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
1353
1358
  } else {
1354
1359
  fragment = this.getHash();
1355
1360
  }
@@ -1365,7 +1370,7 @@
1365
1370
 
1366
1371
  // Figure out the initial configuration. Do we need an iframe?
1367
1372
  // Is pushState desired ... is it available?
1368
- this.options = _.extend({}, {root: '/'}, this.options, options);
1373
+ this.options = _.extend({root: '/'}, this.options, options);
1369
1374
  this.root = this.options.root;
1370
1375
  this._wantsHashChange = this.options.hashChange !== false;
1371
1376
  this._wantsPushState = !!this.options.pushState;
@@ -1378,7 +1383,8 @@
1378
1383
  this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1379
1384
 
1380
1385
  if (oldIE && this._wantsHashChange) {
1381
- this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
1386
+ var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
1387
+ this.iframe = frame.hide().appendTo('body')[0].contentWindow;
1382
1388
  this.navigate(fragment);
1383
1389
  }
1384
1390
 
@@ -1398,19 +1404,25 @@
1398
1404
  var loc = this.location;
1399
1405
  var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1400
1406
 
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;
1407
+ // Transition from hashChange to pushState or vice versa if both are
1408
+ // requested.
1409
+ if (this._wantsHashChange && this._wantsPushState) {
1410
+
1411
+ // If we've started off with a route from a `pushState`-enabled
1412
+ // browser, but we're currently in a browser that doesn't support it...
1413
+ if (!this._hasPushState && !atRoot) {
1414
+ this.fragment = this.getFragment(null, true);
1415
+ this.location.replace(this.root + this.location.search + '#' + this.fragment);
1416
+ // Return immediately as browser will do redirect to new url
1417
+ return true;
1418
+
1419
+ // Or if we've started out with a hash-based route, but we're currently
1420
+ // in a browser where it could be `pushState`-based instead...
1421
+ } else if (this._hasPushState && atRoot && loc.hash) {
1422
+ this.fragment = this.getHash().replace(routeStripper, '');
1423
+ this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1424
+ }
1408
1425
 
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);
1414
1426
  }
1415
1427
 
1416
1428
  if (!this.options.silent) return this.loadUrl();
@@ -1439,21 +1451,20 @@
1439
1451
  }
1440
1452
  if (current === this.fragment) return false;
1441
1453
  if (this.iframe) this.navigate(current);
1442
- this.loadUrl() || this.loadUrl(this.getHash());
1454
+ this.loadUrl();
1443
1455
  },
1444
1456
 
1445
1457
  // Attempt to load the current URL fragment. If a route succeeds with a
1446
1458
  // match, returns `true`. If no defined routes matches the fragment,
1447
1459
  // returns `false`.
1448
- loadUrl: function(fragmentOverride) {
1449
- var fragment = this.fragment = this.getFragment(fragmentOverride);
1450
- var matched = _.any(this.handlers, function(handler) {
1460
+ loadUrl: function(fragment) {
1461
+ fragment = this.fragment = this.getFragment(fragment);
1462
+ return _.any(this.handlers, function(handler) {
1451
1463
  if (handler.route.test(fragment)) {
1452
1464
  handler.callback(fragment);
1453
1465
  return true;
1454
1466
  }
1455
1467
  });
1456
- return matched;
1457
1468
  },
1458
1469
 
1459
1470
  // Save a fragment into the hash history, or replace the URL state if the
@@ -1465,11 +1476,18 @@
1465
1476
  // you wish to modify the current URL without adding an entry to the history.
1466
1477
  navigate: function(fragment, options) {
1467
1478
  if (!History.started) return false;
1468
- if (!options || options === true) options = {trigger: options};
1469
- fragment = this.getFragment(fragment || '');
1479
+ if (!options || options === true) options = {trigger: !!options};
1480
+
1481
+ var url = this.root + (fragment = this.getFragment(fragment || ''));
1482
+
1483
+ // Strip the fragment of the query and hash for matching.
1484
+ fragment = fragment.replace(pathStripper, '');
1485
+
1470
1486
  if (this.fragment === fragment) return;
1471
1487
  this.fragment = fragment;
1472
- var url = this.root + fragment;
1488
+
1489
+ // Don't include a trailing slash on the root.
1490
+ if (fragment === '' && url !== '/') url = url.slice(0, -1);
1473
1491
 
1474
1492
  // If pushState is available, we use it to set the fragment as a real URL.
1475
1493
  if (this._hasPushState) {
@@ -1492,7 +1510,7 @@
1492
1510
  } else {
1493
1511
  return this.location.assign(url);
1494
1512
  }
1495
- if (options.trigger) this.loadUrl(fragment);
1513
+ if (options.trigger) return this.loadUrl(fragment);
1496
1514
  },
1497
1515
 
1498
1516
  // Update the hash location, either replacing the current entry, or adding
@@ -1560,7 +1578,7 @@
1560
1578
  };
1561
1579
 
1562
1580
  // Wrap an optional error callback with a fallback error event.
1563
- var wrapError = function (model, options) {
1581
+ var wrapError = function(model, options) {
1564
1582
  var error = options.error;
1565
1583
  options.error = function(resp) {
1566
1584
  if (error) error(model, resp, options);
@@ -1568,4 +1586,6 @@
1568
1586
  };
1569
1587
  };
1570
1588
 
1571
- }).call(this);
1589
+ return Backbone;
1590
+
1591
+ }));