ultimate-base 0.6.2 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/app/assets/javascripts/ultimate/backbone/app.js.coffee +23 -2
- data/app/assets/javascripts/ultimate/backbone/lib/backbone.js +213 -178
- data/app/assets/javascripts/ultimate/backbone/view.js.coffee +8 -9
- data/app/assets/javascripts/ultimate/helpers.js.coffee.erb +15 -2
- data/app/assets/javascripts/ultimate/underscore/underscore.js +219 -125
- data/app/assets/javascripts/ultimate/underscore/underscore.outcasts.js.coffee +16 -2
- data/app/assets/javascripts/ultimate/underscore/underscore.string.js +38 -9
- data/lib/ultimate/base/version.rb +1 -1
- metadata +2 -2
@@ -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
|
-
@
|
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
|
-
|
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.
|
1
|
+
// Backbone.js 1.1.0
|
2
2
|
|
3
|
-
// (c) 2010-
|
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.
|
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.$ =
|
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
|
155
|
-
if (!
|
156
|
-
var
|
157
|
-
if (typeof name === 'object') callback = this;
|
158
|
-
if (obj) (
|
159
|
-
for (var id in
|
160
|
-
|
161
|
-
|
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
|
219
|
-
var id = obj.
|
220
|
-
|
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
|
-
|
255
|
+
if (options.collection) this.collection = options.collection;
|
252
256
|
if (options.parse) attrs = this.parse(attrs, options) || {};
|
253
|
-
|
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 =
|
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
|
-
//
|
465
|
-
|
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 =
|
536
|
+
var base =
|
537
|
+
_.result(this, 'urlRoot') ||
|
538
|
+
_.result(this.collection, 'url') ||
|
539
|
+
urlError();
|
534
540
|
if (this.isNew()) return base;
|
535
|
-
return base
|
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.
|
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
|
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,
|
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, _.
|
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
|
-
|
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
|
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(
|
671
|
+
options = _.defaults({}, options, setOptions);
|
666
672
|
if (options.parse) models = this.parse(models, options);
|
667
|
-
|
668
|
-
|
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
|
-
|
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(
|
682
|
-
if (
|
683
|
-
if (
|
684
|
-
|
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
|
-
//
|
689
|
-
} else if (
|
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 (
|
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
|
-
|
729
|
+
for (i = 0, l = toAdd.length; i < l; i++) {
|
730
|
+
this.models.splice(at + i, 0, toAdd[i]);
|
731
|
+
}
|
714
732
|
} else {
|
715
|
-
|
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
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
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
|
-
//
|
730
|
-
|
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
|
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
|
-
|
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
|
-
|
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(
|
780
|
-
return this.models
|
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
|
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
|
-
|
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.
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
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', '
|
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
|
-
|
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
|
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' &&
|
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
|
-
|
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.
|
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({
|
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
|
-
|
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
|
-
//
|
1410
|
-
//
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
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()
|
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(
|
1449
|
-
|
1450
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
1604
|
+
return Backbone;
|
1605
|
+
|
1606
|
+
}));
|