backbone-support 0.4.0 → 0.5.0
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.jshintrc +38 -0
- data/.travis.yml +2 -2
- data/CHANGELOG +10 -1
- data/README.md +34 -27
- data/backbone-support.gemspec +1 -1
- data/bower.json +38 -0
- data/lib/assets/javascripts/backbone-support/composite_view.js +8 -8
- data/lib/assets/javascripts/backbone-support/observer.js +25 -13
- data/lib/assets/javascripts/backbone-support/swapping_router.js +1 -1
- data/lib/backbone-support/version.rb +1 -1
- data/spec/javascripts/composite_view_spec.js +46 -66
- data/spec/javascripts/helpers/helpers.js +0 -4
- data/spec/javascripts/observer_spec.js +12 -38
- data/spec/javascripts/support/jasmine.yml +84 -8
- data/spec/javascripts/support/jasmine_helper.rb +15 -0
- data/spec/javascripts/swapping_router_spec.js +29 -46
- data/vendor/assets/javascripts/backbone.js +216 -179
- data/vendor/assets/javascripts/underscore.js +673 -484
- metadata +21 -30
- data/Gemfile.lock +0 -46
- data/spec/javascripts/support/jasmine_config.rb +0 -23
- data/spec/javascripts/support/jasmine_runner.rb +0 -32
@@ -24,7 +24,7 @@ describe('Support.Observer', function() {
|
|
24
24
|
var view, spy, source, callback;
|
25
25
|
beforeEach(function() { Helpers.setup();
|
26
26
|
view = new normalView();
|
27
|
-
spy = sinon.spy(view, "
|
27
|
+
spy = sinon.spy(view, "listenTo");
|
28
28
|
callback = sinon.spy();
|
29
29
|
|
30
30
|
source = new Backbone.Model({
|
@@ -36,63 +36,37 @@ describe('Support.Observer', function() {
|
|
36
36
|
view, spy, source, callback = null;
|
37
37
|
});
|
38
38
|
|
39
|
-
it("
|
40
|
-
view.bindTo(source, 'foobar', callback);
|
41
|
-
expect(view.bindings.length).toEqual(1);
|
42
|
-
});
|
43
|
-
|
44
|
-
it("binds the event to the source object", function() {
|
45
|
-
var mock = sinon.mock(source).expects('bind').once();
|
46
|
-
|
39
|
+
it("calls listenTo on this", function() {
|
47
40
|
view.bindTo(source, 'change:title', callback);
|
48
|
-
|
49
|
-
mock.verify();
|
41
|
+
expect(spy.called).toBeTruthy();
|
50
42
|
});
|
51
43
|
});
|
52
44
|
|
53
45
|
describe("#unbindFromAll", function() {
|
54
46
|
var view, spy, mock;
|
47
|
+
|
55
48
|
beforeEach(function() {
|
56
49
|
view = new normalView();
|
57
50
|
spy = sinon.spy(view, 'unbindFromAll');
|
51
|
+
stopListeningSpy = sinon.spy(view, 'stopListening');
|
58
52
|
callback = sinon.spy();
|
59
53
|
source = new Backbone.Model({
|
60
54
|
title: 'Model or Collection'
|
61
55
|
});
|
62
|
-
unbindSpy = sinon.spy(source, 'unbind');
|
63
|
-
|
64
|
-
runs(function() {
|
65
|
-
view.render();
|
66
|
-
view.bindTo(source, 'foo', callback);
|
67
|
-
view.bindTo(source, 'bar', callback);
|
68
|
-
expect(view.bindings.length).toEqual(2);
|
69
|
-
});
|
70
56
|
|
71
|
-
|
57
|
+
view.render();
|
58
|
+
view.bindTo(source, 'foo', callback);
|
59
|
+
view.bindTo(source, 'bar', callback);
|
72
60
|
|
73
|
-
|
74
|
-
view.leave();
|
75
|
-
});
|
76
|
-
|
77
|
-
Helpers.sleep();
|
61
|
+
view.leave();
|
78
62
|
});
|
79
63
|
|
80
64
|
it("calls the unbindFromAll method when leaving the view", function() {
|
81
|
-
|
82
|
-
expect(spy.called).toBeTruthy();
|
83
|
-
});
|
65
|
+
expect(spy.called).toBeTruthy();
|
84
66
|
});
|
85
67
|
|
86
|
-
it("calls
|
87
|
-
|
88
|
-
expect(unbindSpy.calledTwice).toBeTruthy();
|
89
|
-
});
|
90
|
-
});
|
91
|
-
|
92
|
-
it("removes all the views bindings attached with bindTo", function() {
|
93
|
-
runs(function() {
|
94
|
-
expect(view.bindings.length).toEqual(0);
|
95
|
-
});
|
68
|
+
it("calls stopListening on this", function() {
|
69
|
+
expect(stopListeningSpy.called).toBeTruthy();
|
96
70
|
});
|
97
71
|
});
|
98
72
|
|
@@ -11,17 +11,44 @@
|
|
11
11
|
# - dist/**/*.js
|
12
12
|
#
|
13
13
|
src_files:
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
- spec/javascripts/support/jquery.js
|
15
|
+
- vendor/assets/javascripts/underscore.js
|
16
|
+
- vendor/assets/javascripts/backbone.js
|
17
|
+
- lib/assets/javascripts/backbone-support.js
|
18
|
+
- lib/assets/javascripts/backbone-support/support.js
|
19
|
+
- lib/assets/javascripts/backbone-support/observer.js
|
20
|
+
- lib/**/*.js
|
21
|
+
|
22
|
+
# stylesheets
|
23
|
+
#
|
24
|
+
# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs.
|
25
|
+
# Default: []
|
26
|
+
#
|
27
|
+
# EXAMPLE:
|
28
|
+
#
|
29
|
+
# stylesheets:
|
30
|
+
# - css/style.css
|
31
|
+
# - stylesheets/*.css
|
32
|
+
#
|
33
|
+
stylesheets:
|
34
|
+
- stylesheets/**/*.css
|
35
|
+
|
36
|
+
# helpers
|
37
|
+
#
|
38
|
+
# Return an array of filepaths relative to spec_dir to include before jasmine specs.
|
39
|
+
# Default: ["helpers/**/*.js"]
|
40
|
+
#
|
41
|
+
# EXAMPLE:
|
42
|
+
#
|
43
|
+
# helpers:
|
44
|
+
# - helpers/**/*.js
|
45
|
+
#
|
46
|
+
helpers:
|
47
|
+
- 'helpers/**/*.js'
|
21
48
|
|
22
49
|
# spec_files
|
23
50
|
#
|
24
|
-
#
|
51
|
+
# Return an array of filepaths relative to spec_dir to include.
|
25
52
|
# Default: ["**/*[sS]pec.js"]
|
26
53
|
#
|
27
54
|
# EXAMPLE:
|
@@ -30,6 +57,7 @@ src_files:
|
|
30
57
|
# - **/*[sS]pec.js
|
31
58
|
#
|
32
59
|
spec_files:
|
60
|
+
- '**/*[sS]pec.js'
|
33
61
|
|
34
62
|
# src_dir
|
35
63
|
#
|
@@ -52,3 +80,51 @@ src_dir:
|
|
52
80
|
# spec_dir: spec/javascripts
|
53
81
|
#
|
54
82
|
spec_dir:
|
83
|
+
|
84
|
+
# spec_helper
|
85
|
+
#
|
86
|
+
# Ruby file that Jasmine server will require before starting.
|
87
|
+
# Returned relative to your root path
|
88
|
+
# Default spec/javascripts/support/jasmine_helper.rb
|
89
|
+
#
|
90
|
+
# EXAMPLE:
|
91
|
+
#
|
92
|
+
# spec_helper: spec/javascripts/support/jasmine_helper.rb
|
93
|
+
#
|
94
|
+
spec_helper: spec/javascripts/support/jasmine_helper.rb
|
95
|
+
|
96
|
+
# boot_dir
|
97
|
+
#
|
98
|
+
# Boot directory path. Your boot_files must be returned relative to this path.
|
99
|
+
# Default: Built in boot file
|
100
|
+
#
|
101
|
+
# EXAMPLE:
|
102
|
+
#
|
103
|
+
# boot_dir: spec/javascripts/support/boot
|
104
|
+
#
|
105
|
+
boot_dir:
|
106
|
+
|
107
|
+
# boot_files
|
108
|
+
#
|
109
|
+
# Return an array of filepaths relative to boot_dir to include in order to boot Jasmine
|
110
|
+
# Default: Built in boot file
|
111
|
+
#
|
112
|
+
# EXAMPLE
|
113
|
+
#
|
114
|
+
# boot_files:
|
115
|
+
# - '**/*.js'
|
116
|
+
#
|
117
|
+
boot_files:
|
118
|
+
|
119
|
+
# rack_options
|
120
|
+
#
|
121
|
+
# Extra options to be passed to the rack server
|
122
|
+
# by default, Port and AccessLog are passed.
|
123
|
+
#
|
124
|
+
# This is an advanced options, and left empty by default
|
125
|
+
#
|
126
|
+
# EXAMPLE
|
127
|
+
#
|
128
|
+
# rack_options:
|
129
|
+
# server: 'thin'
|
130
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#Use this file to set/override Jasmine configuration options
|
2
|
+
#You can remove it if you don't need it.
|
3
|
+
#This file is loaded *after* jasmine.yml is interpreted.
|
4
|
+
#
|
5
|
+
#Example: using a different boot file.
|
6
|
+
#Jasmine.configure do |config|
|
7
|
+
# config.boot_dir = '/absolute/path/to/boot_dir'
|
8
|
+
# config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] }
|
9
|
+
#end
|
10
|
+
#
|
11
|
+
#Example: prevent PhantomJS auto install, uses PhantomJS already on your path.
|
12
|
+
#Jasmine.configure do |config|
|
13
|
+
# config.prevent_phantom_js_auto_install = true
|
14
|
+
#end
|
15
|
+
#
|
@@ -60,77 +60,60 @@ describe("Support.SwappingRouter", function() {
|
|
60
60
|
Helpers.teardown();
|
61
61
|
});
|
62
62
|
|
63
|
-
it("should be a backbone router", function() {
|
63
|
+
it("should be a backbone router", function(done) {
|
64
64
|
var spy = sinon.spy();
|
65
65
|
router.bind("route:index", spy);
|
66
66
|
|
67
|
-
|
68
|
-
window.location.hash = "#test"
|
69
|
-
});
|
70
|
-
|
71
|
-
Helpers.sleep();
|
67
|
+
window.location.hash = "#test";
|
72
68
|
|
73
|
-
|
69
|
+
setTimeout(function() {
|
74
70
|
expect(spy.called).toBeTruthy();
|
75
|
-
|
71
|
+
done();
|
72
|
+
}, 30);
|
76
73
|
});
|
77
74
|
|
78
|
-
it("renders and swaps backbone views", function() {
|
79
|
-
|
80
|
-
window.location.hash = "#red"
|
81
|
-
});
|
82
|
-
|
83
|
-
Helpers.sleep();
|
75
|
+
it("renders and swaps backbone views", function(done) {
|
76
|
+
window.location.hash = "#red";
|
84
77
|
|
85
|
-
|
78
|
+
setTimeout(function() {
|
86
79
|
expect($("#test").text()).toEqual("Red!");
|
87
|
-
});
|
88
|
-
|
89
|
-
Helpers.sleep();
|
90
80
|
|
91
|
-
|
92
|
-
window.location.hash = "#blue"
|
93
|
-
});
|
81
|
+
window.location.hash = "#blue";
|
94
82
|
|
95
|
-
|
83
|
+
setTimeout(function() {
|
84
|
+
expect($("#test").text()).toEqual("Blue!");
|
96
85
|
|
97
|
-
|
98
|
-
|
99
|
-
});
|
86
|
+
done();
|
87
|
+
}, 30);
|
88
|
+
}, 30);
|
100
89
|
});
|
101
90
|
|
102
|
-
it("calls leave if it exists on a view", function() {
|
91
|
+
it("calls leave if it exists on a view", function(done) {
|
103
92
|
var spy = sinon.spy(leaveViewInstance, "leave");
|
104
93
|
|
105
|
-
|
106
|
-
window.location.hash = "#leave"
|
107
|
-
});
|
108
|
-
|
109
|
-
Helpers.sleep();
|
94
|
+
window.location.hash = "#leave";
|
110
95
|
|
111
|
-
|
112
|
-
window.location.hash = "#red"
|
113
|
-
});
|
96
|
+
setTimeout(function() {
|
97
|
+
window.location.hash = "#red";
|
114
98
|
|
115
|
-
|
99
|
+
setTimeout(function() {
|
100
|
+
expect(spy.called).toBeTruthy();
|
101
|
+
expect($("#test").text()).toEqual("Red!");
|
116
102
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
});
|
103
|
+
done();
|
104
|
+
}, 30);
|
105
|
+
}, 30);
|
121
106
|
});
|
122
107
|
|
123
|
-
it("calls .swapped on the view after swapping", function() {
|
108
|
+
it("calls .swapped on the view after swapping", function(done) {
|
124
109
|
var spy = sinon.spy(leaveViewInstance, "swapped");
|
125
110
|
|
126
|
-
|
127
|
-
window.location.hash = "#leave"
|
128
|
-
});
|
129
|
-
|
130
|
-
Helpers.sleep()
|
111
|
+
window.location.hash = "#leave";
|
131
112
|
|
132
|
-
|
113
|
+
setTimeout(function() {
|
133
114
|
expect(spy.called).toBeTruthy()
|
115
|
+
|
116
|
+
done();
|
134
117
|
});
|
135
118
|
});
|
136
119
|
});
|
@@ -1,19 +1,35 @@
|
|
1
|
-
// Backbone.js 1.
|
1
|
+
// Backbone.js 1.1.2
|
2
2
|
|
3
|
-
// (c) 2010-
|
3
|
+
// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
4
4
|
// Backbone may be freely distributed under the MIT license.
|
5
5
|
// For all details and documentation:
|
6
6
|
// http://backbonejs.org
|
7
7
|
|
8
|
-
(function(){
|
8
|
+
(function(root, factory) {
|
9
|
+
|
10
|
+
// Set up Backbone appropriately for the environment. Start with AMD.
|
11
|
+
if (typeof define === 'function' && define.amd) {
|
12
|
+
define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
|
13
|
+
// Export global even in AMD case in case this script is loaded with
|
14
|
+
// others that may still expect a global Backbone.
|
15
|
+
root.Backbone = factory(root, exports, _, $);
|
16
|
+
});
|
17
|
+
|
18
|
+
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
|
19
|
+
} else if (typeof exports !== 'undefined') {
|
20
|
+
var _ = require('underscore');
|
21
|
+
factory(root, exports, _);
|
22
|
+
|
23
|
+
// Finally, as a browser global.
|
24
|
+
} else {
|
25
|
+
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
|
26
|
+
}
|
27
|
+
|
28
|
+
}(this, function(root, Backbone, _, $) {
|
9
29
|
|
10
30
|
// Initial Setup
|
11
31
|
// -------------
|
12
32
|
|
13
|
-
// Save a reference to the global object (`window` in the browser, `exports`
|
14
|
-
// on the server).
|
15
|
-
var root = this;
|
16
|
-
|
17
33
|
// Save the previous value of the `Backbone` variable, so that it can be
|
18
34
|
// restored later on, if `noConflict` is used.
|
19
35
|
var previousBackbone = root.Backbone;
|
@@ -24,25 +40,12 @@
|
|
24
40
|
var slice = array.slice;
|
25
41
|
var splice = array.splice;
|
26
42
|
|
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
43
|
// 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');
|
44
|
+
Backbone.VERSION = '1.1.2';
|
42
45
|
|
43
46
|
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
|
44
47
|
// the `$` variable.
|
45
|
-
Backbone.$ =
|
48
|
+
Backbone.$ = $;
|
46
49
|
|
47
50
|
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
|
48
51
|
// to its previous owner. Returns a reference to this Backbone object.
|
@@ -52,7 +55,7 @@
|
|
52
55
|
};
|
53
56
|
|
54
57
|
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
|
55
|
-
// will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
|
58
|
+
// will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
|
56
59
|
// set a `X-Http-Method-Override` header.
|
57
60
|
Backbone.emulateHTTP = false;
|
58
61
|
|
@@ -108,10 +111,9 @@
|
|
108
111
|
var retain, ev, events, names, i, l, j, k;
|
109
112
|
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
|
110
113
|
if (!name && !callback && !context) {
|
111
|
-
this._events =
|
114
|
+
this._events = void 0;
|
112
115
|
return this;
|
113
116
|
}
|
114
|
-
|
115
117
|
names = name ? [name] : _.keys(this._events);
|
116
118
|
for (i = 0, l = names.length; i < l; i++) {
|
117
119
|
name = names[i];
|
@@ -151,14 +153,15 @@
|
|
151
153
|
// Tell this object to stop listening to either specific events ... or
|
152
154
|
// to every object it's currently listening to.
|
153
155
|
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
|
-
|
156
|
+
var listeningTo = this._listeningTo;
|
157
|
+
if (!listeningTo) return this;
|
158
|
+
var remove = !name && !callback;
|
159
|
+
if (!callback && typeof name === 'object') callback = this;
|
160
|
+
if (obj) (listeningTo = {})[obj._listenId] = obj;
|
161
|
+
for (var id in listeningTo) {
|
162
|
+
obj = listeningTo[id];
|
163
|
+
obj.off(name, callback, this);
|
164
|
+
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
|
162
165
|
}
|
163
166
|
return this;
|
164
167
|
}
|
@@ -204,7 +207,7 @@
|
|
204
207
|
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
|
205
208
|
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
|
206
209
|
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);
|
210
|
+
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
|
208
211
|
}
|
209
212
|
};
|
210
213
|
|
@@ -215,10 +218,10 @@
|
|
215
218
|
// listening to.
|
216
219
|
_.each(listenMethods, function(implementation, method) {
|
217
220
|
Events[method] = function(obj, name, callback) {
|
218
|
-
var
|
219
|
-
var id = obj.
|
220
|
-
|
221
|
-
if (typeof name === 'object') callback = this;
|
221
|
+
var listeningTo = this._listeningTo || (this._listeningTo = {});
|
222
|
+
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
|
223
|
+
listeningTo[id] = obj;
|
224
|
+
if (!callback && typeof name === 'object') callback = this;
|
222
225
|
obj[implementation](name, callback, this);
|
223
226
|
return this;
|
224
227
|
};
|
@@ -243,24 +246,18 @@
|
|
243
246
|
// Create a new model with the specified attributes. A client id (`cid`)
|
244
247
|
// is automatically generated and assigned for you.
|
245
248
|
var Model = Backbone.Model = function(attributes, options) {
|
246
|
-
var defaults;
|
247
249
|
var attrs = attributes || {};
|
248
250
|
options || (options = {});
|
249
251
|
this.cid = _.uniqueId('c');
|
250
252
|
this.attributes = {};
|
251
|
-
|
253
|
+
if (options.collection) this.collection = options.collection;
|
252
254
|
if (options.parse) attrs = this.parse(attrs, options) || {};
|
253
|
-
|
254
|
-
attrs = _.defaults({}, attrs, defaults);
|
255
|
-
}
|
255
|
+
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
|
256
256
|
this.set(attrs, options);
|
257
257
|
this.changed = {};
|
258
258
|
this.initialize.apply(this, arguments);
|
259
259
|
};
|
260
260
|
|
261
|
-
// A list of options to be attached directly to the model, if provided.
|
262
|
-
var modelOptions = ['url', 'urlRoot', 'collection'];
|
263
|
-
|
264
261
|
// Attach all inheritable methods to the Model prototype.
|
265
262
|
_.extend(Model.prototype, Events, {
|
266
263
|
|
@@ -355,7 +352,7 @@
|
|
355
352
|
|
356
353
|
// Trigger all relevant attribute changes.
|
357
354
|
if (!silent) {
|
358
|
-
if (changes.length) this._pending =
|
355
|
+
if (changes.length) this._pending = options;
|
359
356
|
for (var i = 0, l = changes.length; i < l; i++) {
|
360
357
|
this.trigger('change:' + changes[i], this, current[changes[i]], options);
|
361
358
|
}
|
@@ -366,6 +363,7 @@
|
|
366
363
|
if (changing) return this;
|
367
364
|
if (!silent) {
|
368
365
|
while (this._pending) {
|
366
|
+
options = this._pending;
|
369
367
|
this._pending = false;
|
370
368
|
this.trigger('change', this, options);
|
371
369
|
}
|
@@ -456,13 +454,16 @@
|
|
456
454
|
(attrs = {})[key] = val;
|
457
455
|
}
|
458
456
|
|
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
457
|
options = _.extend({validate: true}, options);
|
463
458
|
|
464
|
-
//
|
465
|
-
|
459
|
+
// If we're not waiting and attributes exist, save acts as
|
460
|
+
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
461
|
+
// the model will be valid when the attributes, if any, are set.
|
462
|
+
if (attrs && !options.wait) {
|
463
|
+
if (!this.set(attrs, options)) return false;
|
464
|
+
} else {
|
465
|
+
if (!this._validate(attrs, options)) return false;
|
466
|
+
}
|
466
467
|
|
467
468
|
// Set temporary attributes if `{wait: true}`.
|
468
469
|
if (attrs && options.wait) {
|
@@ -530,9 +531,12 @@
|
|
530
531
|
// using Backbone's restful methods, override this to change the endpoint
|
531
532
|
// that will be called.
|
532
533
|
url: function() {
|
533
|
-
var base =
|
534
|
+
var base =
|
535
|
+
_.result(this, 'urlRoot') ||
|
536
|
+
_.result(this.collection, 'url') ||
|
537
|
+
urlError();
|
534
538
|
if (this.isNew()) return base;
|
535
|
-
return base
|
539
|
+
return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
|
536
540
|
},
|
537
541
|
|
538
542
|
// **parse** converts a response into the hash of attributes to be `set` on
|
@@ -548,7 +552,7 @@
|
|
548
552
|
|
549
553
|
// A model is new if it has never been saved to the server, and lacks an id.
|
550
554
|
isNew: function() {
|
551
|
-
return this.
|
555
|
+
return !this.has(this.idAttribute);
|
552
556
|
},
|
553
557
|
|
554
558
|
// Check if the model is currently in a valid state.
|
@@ -563,7 +567,7 @@
|
|
563
567
|
attrs = _.extend({}, this.attributes, attrs);
|
564
568
|
var error = this.validationError = this.validate(attrs, options) || null;
|
565
569
|
if (!error) return true;
|
566
|
-
this.trigger('invalid', this, error, _.extend(options
|
570
|
+
this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
|
567
571
|
return false;
|
568
572
|
}
|
569
573
|
|
@@ -596,7 +600,6 @@
|
|
596
600
|
// its models in sort order, as they're added and removed.
|
597
601
|
var Collection = Backbone.Collection = function(models, options) {
|
598
602
|
options || (options = {});
|
599
|
-
if (options.url) this.url = options.url;
|
600
603
|
if (options.model) this.model = options.model;
|
601
604
|
if (options.comparator !== void 0) this.comparator = options.comparator;
|
602
605
|
this._reset();
|
@@ -606,7 +609,7 @@
|
|
606
609
|
|
607
610
|
// Default options for `Collection#set`.
|
608
611
|
var setOptions = {add: true, remove: true, merge: true};
|
609
|
-
var addOptions = {add: true,
|
612
|
+
var addOptions = {add: true, remove: false};
|
610
613
|
|
611
614
|
// Define the Collection's inheritable methods.
|
612
615
|
_.extend(Collection.prototype, Events, {
|
@@ -632,16 +635,17 @@
|
|
632
635
|
|
633
636
|
// Add a model, or list of models to the set.
|
634
637
|
add: function(models, options) {
|
635
|
-
return this.set(models, _.
|
638
|
+
return this.set(models, _.extend({merge: false}, options, addOptions));
|
636
639
|
},
|
637
640
|
|
638
641
|
// Remove a model, or a list of models from the set.
|
639
642
|
remove: function(models, options) {
|
640
|
-
|
643
|
+
var singular = !_.isArray(models);
|
644
|
+
models = singular ? [models] : _.clone(models);
|
641
645
|
options || (options = {});
|
642
646
|
var i, l, index, model;
|
643
647
|
for (i = 0, l = models.length; i < l; i++) {
|
644
|
-
model = this.get(models[i]);
|
648
|
+
model = models[i] = this.get(models[i]);
|
645
649
|
if (!model) continue;
|
646
650
|
delete this._byId[model.id];
|
647
651
|
delete this._byId[model.cid];
|
@@ -652,9 +656,9 @@
|
|
652
656
|
options.index = index;
|
653
657
|
model.trigger('remove', model, this, options);
|
654
658
|
}
|
655
|
-
this._removeReference(model);
|
659
|
+
this._removeReference(model, options);
|
656
660
|
}
|
657
|
-
return
|
661
|
+
return singular ? models[0] : models;
|
658
662
|
},
|
659
663
|
|
660
664
|
// Update a collection by `set`-ing a new list of models, adding new ones,
|
@@ -662,43 +666,57 @@
|
|
662
666
|
// already exist in the collection, as necessary. Similar to **Model#set**,
|
663
667
|
// the core operation for updating the data contained by the collection.
|
664
668
|
set: function(models, options) {
|
665
|
-
options = _.defaults(
|
669
|
+
options = _.defaults({}, options, setOptions);
|
666
670
|
if (options.parse) models = this.parse(models, options);
|
667
|
-
|
668
|
-
|
671
|
+
var singular = !_.isArray(models);
|
672
|
+
models = singular ? (models ? [models] : []) : _.clone(models);
|
673
|
+
var i, l, id, model, attrs, existing, sort;
|
669
674
|
var at = options.at;
|
675
|
+
var targetModel = this.model;
|
670
676
|
var sortable = this.comparator && (at == null) && options.sort !== false;
|
671
677
|
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
|
672
678
|
var toAdd = [], toRemove = [], modelMap = {};
|
679
|
+
var add = options.add, merge = options.merge, remove = options.remove;
|
680
|
+
var order = !sortable && add && remove ? [] : false;
|
673
681
|
|
674
682
|
// Turn bare objects into model references, and prevent invalid models
|
675
683
|
// from being added.
|
676
684
|
for (i = 0, l = models.length; i < l; i++) {
|
677
|
-
|
685
|
+
attrs = models[i] || {};
|
686
|
+
if (attrs instanceof Model) {
|
687
|
+
id = model = attrs;
|
688
|
+
} else {
|
689
|
+
id = attrs[targetModel.prototype.idAttribute || 'id'];
|
690
|
+
}
|
678
691
|
|
679
692
|
// If a duplicate is found, prevent it from being added and
|
680
693
|
// optionally merge it into the existing model.
|
681
|
-
if (existing = this.get(
|
682
|
-
if (
|
683
|
-
if (
|
684
|
-
|
694
|
+
if (existing = this.get(id)) {
|
695
|
+
if (remove) modelMap[existing.cid] = true;
|
696
|
+
if (merge) {
|
697
|
+
attrs = attrs === model ? model.attributes : attrs;
|
698
|
+
if (options.parse) attrs = existing.parse(attrs, options);
|
699
|
+
existing.set(attrs, options);
|
685
700
|
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
|
686
701
|
}
|
702
|
+
models[i] = existing;
|
687
703
|
|
688
|
-
//
|
689
|
-
} else if (
|
704
|
+
// If this is a new, valid model, push it to the `toAdd` list.
|
705
|
+
} else if (add) {
|
706
|
+
model = models[i] = this._prepareModel(attrs, options);
|
707
|
+
if (!model) continue;
|
690
708
|
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;
|
709
|
+
this._addReference(model, options);
|
697
710
|
}
|
711
|
+
|
712
|
+
// Do not add multiple models with the same `id`.
|
713
|
+
model = existing || model;
|
714
|
+
if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
|
715
|
+
modelMap[model.id] = true;
|
698
716
|
}
|
699
717
|
|
700
718
|
// Remove nonexistent models if appropriate.
|
701
|
-
if (
|
719
|
+
if (remove) {
|
702
720
|
for (i = 0, l = this.length; i < l; ++i) {
|
703
721
|
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
|
704
722
|
}
|
@@ -706,29 +724,35 @@
|
|
706
724
|
}
|
707
725
|
|
708
726
|
// See if sorting is needed, update `length` and splice in new models.
|
709
|
-
if (toAdd.length) {
|
727
|
+
if (toAdd.length || (order && order.length)) {
|
710
728
|
if (sortable) sort = true;
|
711
729
|
this.length += toAdd.length;
|
712
730
|
if (at != null) {
|
713
|
-
|
731
|
+
for (i = 0, l = toAdd.length; i < l; i++) {
|
732
|
+
this.models.splice(at + i, 0, toAdd[i]);
|
733
|
+
}
|
714
734
|
} else {
|
715
|
-
|
735
|
+
if (order) this.models.length = 0;
|
736
|
+
var orderedModels = order || toAdd;
|
737
|
+
for (i = 0, l = orderedModels.length; i < l; i++) {
|
738
|
+
this.models.push(orderedModels[i]);
|
739
|
+
}
|
716
740
|
}
|
717
741
|
}
|
718
742
|
|
719
743
|
// Silently sort the collection if appropriate.
|
720
744
|
if (sort) this.sort({silent: true});
|
721
745
|
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
746
|
+
// Unless silenced, it's time to fire all appropriate add/sort events.
|
747
|
+
if (!options.silent) {
|
748
|
+
for (i = 0, l = toAdd.length; i < l; i++) {
|
749
|
+
(model = toAdd[i]).trigger('add', model, this, options);
|
750
|
+
}
|
751
|
+
if (sort || (order && order.length)) this.trigger('sort', this, options);
|
727
752
|
}
|
728
753
|
|
729
|
-
//
|
730
|
-
|
731
|
-
return this;
|
754
|
+
// Return the added (or merged) model (or models).
|
755
|
+
return singular ? models[0] : models;
|
732
756
|
},
|
733
757
|
|
734
758
|
// When you have more items than you want to add or remove individually,
|
@@ -738,20 +762,18 @@
|
|
738
762
|
reset: function(models, options) {
|
739
763
|
options || (options = {});
|
740
764
|
for (var i = 0, l = this.models.length; i < l; i++) {
|
741
|
-
this._removeReference(this.models[i]);
|
765
|
+
this._removeReference(this.models[i], options);
|
742
766
|
}
|
743
767
|
options.previousModels = this.models;
|
744
768
|
this._reset();
|
745
|
-
this.add(models, _.extend({silent: true}, options));
|
769
|
+
models = this.add(models, _.extend({silent: true}, options));
|
746
770
|
if (!options.silent) this.trigger('reset', this, options);
|
747
|
-
return
|
771
|
+
return models;
|
748
772
|
},
|
749
773
|
|
750
774
|
// Add a model to the end of the collection.
|
751
775
|
push: function(model, options) {
|
752
|
-
|
753
|
-
this.add(model, _.extend({at: this.length}, options));
|
754
|
-
return model;
|
776
|
+
return this.add(model, _.extend({at: this.length}, options));
|
755
777
|
},
|
756
778
|
|
757
779
|
// Remove a model from the end of the collection.
|
@@ -763,9 +785,7 @@
|
|
763
785
|
|
764
786
|
// Add a model to the beginning of the collection.
|
765
787
|
unshift: function(model, options) {
|
766
|
-
|
767
|
-
this.add(model, _.extend({at: 0}, options));
|
768
|
-
return model;
|
788
|
+
return this.add(model, _.extend({at: 0}, options));
|
769
789
|
},
|
770
790
|
|
771
791
|
// Remove a model from the beginning of the collection.
|
@@ -776,14 +796,14 @@
|
|
776
796
|
},
|
777
797
|
|
778
798
|
// Slice out a sub-array of models from the collection.
|
779
|
-
slice: function(
|
780
|
-
return this.models
|
799
|
+
slice: function() {
|
800
|
+
return slice.apply(this.models, arguments);
|
781
801
|
},
|
782
802
|
|
783
803
|
// Get a model from the set by id.
|
784
804
|
get: function(obj) {
|
785
805
|
if (obj == null) return void 0;
|
786
|
-
return this._byId[obj
|
806
|
+
return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
|
787
807
|
},
|
788
808
|
|
789
809
|
// Get the model at the given index.
|
@@ -827,16 +847,6 @@
|
|
827
847
|
return this;
|
828
848
|
},
|
829
849
|
|
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
850
|
// Pluck an attribute from each model in the collection.
|
841
851
|
pluck: function(attr) {
|
842
852
|
return _.invoke(this.models, 'get', attr);
|
@@ -869,7 +879,7 @@
|
|
869
879
|
if (!options.wait) this.add(model, options);
|
870
880
|
var collection = this;
|
871
881
|
var success = options.success;
|
872
|
-
options.success = function(resp) {
|
882
|
+
options.success = function(model, resp) {
|
873
883
|
if (options.wait) collection.add(model, options);
|
874
884
|
if (success) success(model, resp, options);
|
875
885
|
};
|
@@ -899,22 +909,25 @@
|
|
899
909
|
// Prepare a hash of attributes (or other model) to be added to this
|
900
910
|
// collection.
|
901
911
|
_prepareModel: function(attrs, options) {
|
902
|
-
if (attrs instanceof Model)
|
903
|
-
|
904
|
-
return attrs;
|
905
|
-
}
|
906
|
-
options || (options = {});
|
912
|
+
if (attrs instanceof Model) return attrs;
|
913
|
+
options = options ? _.clone(options) : {};
|
907
914
|
options.collection = this;
|
908
915
|
var model = new this.model(attrs, options);
|
909
|
-
if (!model.
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
916
|
+
if (!model.validationError) return model;
|
917
|
+
this.trigger('invalid', this, model.validationError, options);
|
918
|
+
return false;
|
919
|
+
},
|
920
|
+
|
921
|
+
// Internal method to create a model's ties to a collection.
|
922
|
+
_addReference: function(model, options) {
|
923
|
+
this._byId[model.cid] = model;
|
924
|
+
if (model.id != null) this._byId[model.id] = model;
|
925
|
+
if (!model.collection) model.collection = this;
|
926
|
+
model.on('all', this._onModelEvent, this);
|
914
927
|
},
|
915
928
|
|
916
929
|
// Internal method to sever a model's ties to a collection.
|
917
|
-
_removeReference: function(model) {
|
930
|
+
_removeReference: function(model, options) {
|
918
931
|
if (this === model.collection) delete model.collection;
|
919
932
|
model.off('all', this._onModelEvent, this);
|
920
933
|
},
|
@@ -942,8 +955,8 @@
|
|
942
955
|
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
|
943
956
|
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
|
944
957
|
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
|
945
|
-
'tail', 'drop', 'last', 'without', '
|
946
|
-
'isEmpty', 'chain'];
|
958
|
+
'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
|
959
|
+
'lastIndexOf', 'isEmpty', 'chain', 'sample'];
|
947
960
|
|
948
961
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
949
962
|
_.each(methods, function(method) {
|
@@ -955,7 +968,7 @@
|
|
955
968
|
});
|
956
969
|
|
957
970
|
// Underscore methods that take a property name as an argument.
|
958
|
-
var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
|
971
|
+
var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
|
959
972
|
|
960
973
|
// Use attributes instead of properties.
|
961
974
|
_.each(attributeMethods, function(method) {
|
@@ -982,7 +995,8 @@
|
|
982
995
|
// if an existing element is not provided...
|
983
996
|
var View = Backbone.View = function(options) {
|
984
997
|
this.cid = _.uniqueId('view');
|
985
|
-
|
998
|
+
options || (options = {});
|
999
|
+
_.extend(this, _.pick(options, viewOptions));
|
986
1000
|
this._ensureElement();
|
987
1001
|
this.initialize.apply(this, arguments);
|
988
1002
|
this.delegateEvents();
|
@@ -1001,7 +1015,7 @@
|
|
1001
1015
|
tagName: 'div',
|
1002
1016
|
|
1003
1017
|
// jQuery delegate for element lookup, scoped to DOM elements within the
|
1004
|
-
// current view. This should be
|
1018
|
+
// current view. This should be preferred to global lookups where possible.
|
1005
1019
|
$: function(selector) {
|
1006
1020
|
return this.$el.find(selector);
|
1007
1021
|
},
|
@@ -1041,7 +1055,7 @@
|
|
1041
1055
|
//
|
1042
1056
|
// {
|
1043
1057
|
// 'mousedown .title': 'edit',
|
1044
|
-
// 'click .button': 'save'
|
1058
|
+
// 'click .button': 'save',
|
1045
1059
|
// 'click .open': function(e) { ... }
|
1046
1060
|
// }
|
1047
1061
|
//
|
@@ -1079,16 +1093,6 @@
|
|
1079
1093
|
return this;
|
1080
1094
|
},
|
1081
1095
|
|
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
1096
|
// Ensure that the View has a DOM element to render into.
|
1093
1097
|
// If `this.el` is a string, pass it through `$()`, take the first
|
1094
1098
|
// matching element, and re-assign it to `el`. Otherwise, create
|
@@ -1174,8 +1178,7 @@
|
|
1174
1178
|
// If we're sending a `PATCH` request, and we're in an old Internet Explorer
|
1175
1179
|
// that still has ActiveX enabled by default, override jQuery to use that
|
1176
1180
|
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
|
1177
|
-
if (params.type === 'PATCH' &&
|
1178
|
-
!(window.external && window.external.msActiveXFilteringEnabled)) {
|
1181
|
+
if (params.type === 'PATCH' && noXhrPatch) {
|
1179
1182
|
params.xhr = function() {
|
1180
1183
|
return new ActiveXObject("Microsoft.XMLHTTP");
|
1181
1184
|
};
|
@@ -1187,6 +1190,10 @@
|
|
1187
1190
|
return xhr;
|
1188
1191
|
};
|
1189
1192
|
|
1193
|
+
var noXhrPatch =
|
1194
|
+
typeof window !== 'undefined' && !!window.ActiveXObject &&
|
1195
|
+
!(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
|
1196
|
+
|
1190
1197
|
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
1191
1198
|
var methodMap = {
|
1192
1199
|
'create': 'POST',
|
@@ -1244,7 +1251,7 @@
|
|
1244
1251
|
var router = this;
|
1245
1252
|
Backbone.history.route(route, function(fragment) {
|
1246
1253
|
var args = router._extractParameters(route, fragment);
|
1247
|
-
|
1254
|
+
router.execute(callback, args);
|
1248
1255
|
router.trigger.apply(router, ['route:' + name].concat(args));
|
1249
1256
|
router.trigger('route', name, args);
|
1250
1257
|
Backbone.history.trigger('route', router, name, args);
|
@@ -1252,6 +1259,12 @@
|
|
1252
1259
|
return this;
|
1253
1260
|
},
|
1254
1261
|
|
1262
|
+
// Execute a route handler with the provided parameters. This is an
|
1263
|
+
// excellent place to do pre-route setup or post-route cleanup.
|
1264
|
+
execute: function(callback, args) {
|
1265
|
+
if (callback) callback.apply(this, args);
|
1266
|
+
},
|
1267
|
+
|
1255
1268
|
// Simple proxy to `Backbone.history` to save a fragment into the history.
|
1256
1269
|
navigate: function(fragment, options) {
|
1257
1270
|
Backbone.history.navigate(fragment, options);
|
@@ -1275,11 +1288,11 @@
|
|
1275
1288
|
_routeToRegExp: function(route) {
|
1276
1289
|
route = route.replace(escapeRegExp, '\\$&')
|
1277
1290
|
.replace(optionalParam, '(?:$1)?')
|
1278
|
-
.replace(namedParam, function(match, optional){
|
1279
|
-
return optional ? match : '([
|
1291
|
+
.replace(namedParam, function(match, optional) {
|
1292
|
+
return optional ? match : '([^/?]+)';
|
1280
1293
|
})
|
1281
|
-
.replace(splatParam, '(
|
1282
|
-
return new RegExp('^' + route + '
|
1294
|
+
.replace(splatParam, '([^?]*?)');
|
1295
|
+
return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
|
1283
1296
|
},
|
1284
1297
|
|
1285
1298
|
// Given a route, and a URL fragment that it matches, return the array of
|
@@ -1287,7 +1300,9 @@
|
|
1287
1300
|
// treated as `null` to normalize cross-browser behavior.
|
1288
1301
|
_extractParameters: function(route, fragment) {
|
1289
1302
|
var params = route.exec(fragment).slice(1);
|
1290
|
-
return _.map(params, function(param) {
|
1303
|
+
return _.map(params, function(param, i) {
|
1304
|
+
// Don't decode the search params.
|
1305
|
+
if (i === params.length - 1) return param || null;
|
1291
1306
|
return param ? decodeURIComponent(param) : null;
|
1292
1307
|
});
|
1293
1308
|
}
|
@@ -1325,6 +1340,9 @@
|
|
1325
1340
|
// Cached regex for removing a trailing slash.
|
1326
1341
|
var trailingSlash = /\/$/;
|
1327
1342
|
|
1343
|
+
// Cached regex for stripping urls of hash.
|
1344
|
+
var pathStripper = /#.*$/;
|
1345
|
+
|
1328
1346
|
// Has the history handling already been started?
|
1329
1347
|
History.started = false;
|
1330
1348
|
|
@@ -1335,6 +1353,11 @@
|
|
1335
1353
|
// twenty times a second.
|
1336
1354
|
interval: 50,
|
1337
1355
|
|
1356
|
+
// Are we at the app root?
|
1357
|
+
atRoot: function() {
|
1358
|
+
return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
|
1359
|
+
},
|
1360
|
+
|
1338
1361
|
// Gets the true hash value. Cannot use location.hash directly due to bug
|
1339
1362
|
// in Firefox where location.hash will always be decoded.
|
1340
1363
|
getHash: function(window) {
|
@@ -1347,9 +1370,9 @@
|
|
1347
1370
|
getFragment: function(fragment, forcePushState) {
|
1348
1371
|
if (fragment == null) {
|
1349
1372
|
if (this._hasPushState || !this._wantsHashChange || forcePushState) {
|
1350
|
-
fragment = this.location.pathname;
|
1373
|
+
fragment = decodeURI(this.location.pathname + this.location.search);
|
1351
1374
|
var root = this.root.replace(trailingSlash, '');
|
1352
|
-
if (!fragment.indexOf(root)) fragment = fragment.
|
1375
|
+
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
|
1353
1376
|
} else {
|
1354
1377
|
fragment = this.getHash();
|
1355
1378
|
}
|
@@ -1365,7 +1388,7 @@
|
|
1365
1388
|
|
1366
1389
|
// Figure out the initial configuration. Do we need an iframe?
|
1367
1390
|
// Is pushState desired ... is it available?
|
1368
|
-
this.options = _.extend({
|
1391
|
+
this.options = _.extend({root: '/'}, this.options, options);
|
1369
1392
|
this.root = this.options.root;
|
1370
1393
|
this._wantsHashChange = this.options.hashChange !== false;
|
1371
1394
|
this._wantsPushState = !!this.options.pushState;
|
@@ -1378,7 +1401,8 @@
|
|
1378
1401
|
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
|
1379
1402
|
|
1380
1403
|
if (oldIE && this._wantsHashChange) {
|
1381
|
-
|
1404
|
+
var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
|
1405
|
+
this.iframe = frame.hide().appendTo('body')[0].contentWindow;
|
1382
1406
|
this.navigate(fragment);
|
1383
1407
|
}
|
1384
1408
|
|
@@ -1396,21 +1420,26 @@
|
|
1396
1420
|
// opened by a non-pushState browser.
|
1397
1421
|
this.fragment = fragment;
|
1398
1422
|
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
1423
|
|
1409
|
-
//
|
1410
|
-
//
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1424
|
+
// Transition from hashChange to pushState or vice versa if both are
|
1425
|
+
// requested.
|
1426
|
+
if (this._wantsHashChange && this._wantsPushState) {
|
1427
|
+
|
1428
|
+
// If we've started off with a route from a `pushState`-enabled
|
1429
|
+
// browser, but we're currently in a browser that doesn't support it...
|
1430
|
+
if (!this._hasPushState && !this.atRoot()) {
|
1431
|
+
this.fragment = this.getFragment(null, true);
|
1432
|
+
this.location.replace(this.root + '#' + this.fragment);
|
1433
|
+
// Return immediately as browser will do redirect to new url
|
1434
|
+
return true;
|
1435
|
+
|
1436
|
+
// Or if we've started out with a hash-based route, but we're currently
|
1437
|
+
// in a browser where it could be `pushState`-based instead...
|
1438
|
+
} else if (this._hasPushState && this.atRoot() && loc.hash) {
|
1439
|
+
this.fragment = this.getHash().replace(routeStripper, '');
|
1440
|
+
this.history.replaceState({}, document.title, this.root + this.fragment);
|
1441
|
+
}
|
1442
|
+
|
1414
1443
|
}
|
1415
1444
|
|
1416
1445
|
if (!this.options.silent) return this.loadUrl();
|
@@ -1420,7 +1449,7 @@
|
|
1420
1449
|
// but possibly useful for unit testing Routers.
|
1421
1450
|
stop: function() {
|
1422
1451
|
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
|
1423
|
-
clearInterval(this._checkUrlInterval);
|
1452
|
+
if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
|
1424
1453
|
History.started = false;
|
1425
1454
|
},
|
1426
1455
|
|
@@ -1439,21 +1468,20 @@
|
|
1439
1468
|
}
|
1440
1469
|
if (current === this.fragment) return false;
|
1441
1470
|
if (this.iframe) this.navigate(current);
|
1442
|
-
this.loadUrl()
|
1471
|
+
this.loadUrl();
|
1443
1472
|
},
|
1444
1473
|
|
1445
1474
|
// Attempt to load the current URL fragment. If a route succeeds with a
|
1446
1475
|
// match, returns `true`. If no defined routes matches the fragment,
|
1447
1476
|
// returns `false`.
|
1448
|
-
loadUrl: function(
|
1449
|
-
|
1450
|
-
|
1477
|
+
loadUrl: function(fragment) {
|
1478
|
+
fragment = this.fragment = this.getFragment(fragment);
|
1479
|
+
return _.any(this.handlers, function(handler) {
|
1451
1480
|
if (handler.route.test(fragment)) {
|
1452
1481
|
handler.callback(fragment);
|
1453
1482
|
return true;
|
1454
1483
|
}
|
1455
1484
|
});
|
1456
|
-
return matched;
|
1457
1485
|
},
|
1458
1486
|
|
1459
1487
|
// Save a fragment into the hash history, or replace the URL state if the
|
@@ -1465,11 +1493,18 @@
|
|
1465
1493
|
// you wish to modify the current URL without adding an entry to the history.
|
1466
1494
|
navigate: function(fragment, options) {
|
1467
1495
|
if (!History.started) return false;
|
1468
|
-
if (!options || options === true) options = {trigger: options};
|
1469
|
-
|
1496
|
+
if (!options || options === true) options = {trigger: !!options};
|
1497
|
+
|
1498
|
+
var url = this.root + (fragment = this.getFragment(fragment || ''));
|
1499
|
+
|
1500
|
+
// Strip the hash for matching.
|
1501
|
+
fragment = fragment.replace(pathStripper, '');
|
1502
|
+
|
1470
1503
|
if (this.fragment === fragment) return;
|
1471
1504
|
this.fragment = fragment;
|
1472
|
-
|
1505
|
+
|
1506
|
+
// Don't include a trailing slash on the root.
|
1507
|
+
if (fragment === '' && url !== '/') url = url.slice(0, -1);
|
1473
1508
|
|
1474
1509
|
// If pushState is available, we use it to set the fragment as a real URL.
|
1475
1510
|
if (this._hasPushState) {
|
@@ -1492,7 +1527,7 @@
|
|
1492
1527
|
} else {
|
1493
1528
|
return this.location.assign(url);
|
1494
1529
|
}
|
1495
|
-
if (options.trigger) this.loadUrl(fragment);
|
1530
|
+
if (options.trigger) return this.loadUrl(fragment);
|
1496
1531
|
},
|
1497
1532
|
|
1498
1533
|
// Update the hash location, either replacing the current entry, or adding
|
@@ -1560,7 +1595,7 @@
|
|
1560
1595
|
};
|
1561
1596
|
|
1562
1597
|
// Wrap an optional error callback with a fallback error event.
|
1563
|
-
var wrapError = function
|
1598
|
+
var wrapError = function(model, options) {
|
1564
1599
|
var error = options.error;
|
1565
1600
|
options.error = function(resp) {
|
1566
1601
|
if (error) error(model, resp, options);
|
@@ -1568,4 +1603,6 @@
|
|
1568
1603
|
};
|
1569
1604
|
};
|
1570
1605
|
|
1571
|
-
|
1606
|
+
return Backbone;
|
1607
|
+
|
1608
|
+
}));
|