rails-backbone 0.8.0 → 0.9.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.
- data/README.md +2 -2
- data/lib/generators/backbone/router/templates/view.coffee +1 -1
- data/lib/generators/backbone/scaffold/templates/templates/edit.jst +1 -1
- data/lib/generators/backbone/scaffold/templates/views/edit_view.coffee +7 -7
- data/lib/generators/backbone/scaffold/templates/views/index_view.coffee +1 -1
- data/lib/generators/backbone/scaffold/templates/views/model_view.coffee +1 -1
- data/lib/generators/backbone/scaffold/templates/views/new_view.coffee +1 -1
- data/lib/generators/backbone/scaffold/templates/views/show_view.coffee +1 -1
- data/vendor/assets/javascripts/backbone.js +543 -441
- data/vendor/assets/javascripts/underscore.js +87 -52
- metadata +82 -27
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Backbone-Rails [](http://travis-ci.org/codebrew/backbone-rails)
|
2
2
|
|
3
|
-
Easily setup and use backbone.js (0.9.
|
3
|
+
Easily setup and use backbone.js (0.9.9) with rails 3.1 and greater
|
4
4
|
|
5
5
|
Follow [@TheRyanFitz on Twitter](http://twitter.com/#!/TheRyanFitz). Tweet any questions or suggestions you have about the project.
|
6
6
|
|
@@ -98,4 +98,4 @@ If you prefer haml, this is equivalent to inserting the following code into `app
|
|
98
98
|
|
99
99
|
|
100
100
|
Now start your server `rails s` and browse to [localhost:3000/posts](http://localhost:3000/posts)
|
101
|
-
You should now have a fully functioning single page crud app for Post models.
|
101
|
+
You should now have a fully functioning single page crud app for Post models.
|
@@ -1,23 +1,23 @@
|
|
1
1
|
<%= view_namespace %> ||= {}
|
2
2
|
|
3
3
|
class <%= view_namespace %>.EditView extends Backbone.View
|
4
|
-
template
|
4
|
+
template: JST["<%= jst 'edit' %>"]
|
5
5
|
|
6
|
-
events
|
7
|
-
"submit #edit-<%= singular_name %>"
|
6
|
+
events:
|
7
|
+
"submit #edit-<%= singular_name %>": "update"
|
8
8
|
|
9
|
-
update
|
9
|
+
update: (e) ->
|
10
10
|
e.preventDefault()
|
11
11
|
e.stopPropagation()
|
12
12
|
|
13
13
|
@model.save(null,
|
14
|
-
success
|
14
|
+
success: (<%= singular_name %>) =>
|
15
15
|
@model = <%= singular_name %>
|
16
16
|
window.location.hash = "/#{@model.id}"
|
17
17
|
)
|
18
18
|
|
19
|
-
render
|
20
|
-
|
19
|
+
render: ->
|
20
|
+
@$el.html(@template(@model.toJSON() ))
|
21
21
|
|
22
22
|
this.$("form").backboneLink(@model)
|
23
23
|
|
@@ -14,7 +14,7 @@ class <%= view_namespace %>.IndexView extends Backbone.View
|
|
14
14
|
@$("tbody").append(view.render().el)
|
15
15
|
|
16
16
|
render: =>
|
17
|
-
|
17
|
+
@$el.html(@template(<%= plural_model_name %>: @options.<%= plural_model_name %>.toJSON() ))
|
18
18
|
@addAll()
|
19
19
|
|
20
20
|
return this
|
@@ -1,4 +1,4 @@
|
|
1
|
-
// Backbone.js 0.9.
|
1
|
+
// Backbone.js 0.9.9
|
2
2
|
|
3
3
|
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
4
4
|
// Backbone may be freely distributed under the MIT license.
|
@@ -10,7 +10,7 @@
|
|
10
10
|
// Initial Setup
|
11
11
|
// -------------
|
12
12
|
|
13
|
-
// Save a reference to the global object (`window` in the browser, `
|
13
|
+
// Save a reference to the global object (`window` in the browser, `exports`
|
14
14
|
// on the server).
|
15
15
|
var root = this;
|
16
16
|
|
@@ -18,9 +18,11 @@
|
|
18
18
|
// restored later on, if `noConflict` is used.
|
19
19
|
var previousBackbone = root.Backbone;
|
20
20
|
|
21
|
-
// Create a local reference to
|
22
|
-
var
|
23
|
-
var
|
21
|
+
// Create a local reference to array methods.
|
22
|
+
var array = [];
|
23
|
+
var push = array.push;
|
24
|
+
var slice = array.slice;
|
25
|
+
var splice = array.splice;
|
24
26
|
|
25
27
|
// The top-level namespace. All public Backbone classes and modules will
|
26
28
|
// be attached to this. Exported for both CommonJS and the browser.
|
@@ -32,23 +34,14 @@
|
|
32
34
|
}
|
33
35
|
|
34
36
|
// Current version of the library. Keep in sync with `package.json`.
|
35
|
-
Backbone.VERSION = '0.9.
|
37
|
+
Backbone.VERSION = '0.9.9';
|
36
38
|
|
37
39
|
// Require Underscore, if we're on the server, and it's not already present.
|
38
40
|
var _ = root._;
|
39
41
|
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
|
40
42
|
|
41
43
|
// For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
|
42
|
-
|
43
|
-
|
44
|
-
// Set the JavaScript library that will be used for DOM manipulation and
|
45
|
-
// Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
|
46
|
-
// Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
|
47
|
-
// alternate JavaScript library (or a mock library for testing your views
|
48
|
-
// outside of a browser).
|
49
|
-
Backbone.setDomLibrary = function(lib) {
|
50
|
-
$ = lib;
|
51
|
-
};
|
44
|
+
Backbone.$ = root.jQuery || root.Zepto || root.ender;
|
52
45
|
|
53
46
|
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
|
54
47
|
// to its previous owner. Returns a reference to this Backbone object.
|
@@ -69,14 +62,51 @@
|
|
69
62
|
Backbone.emulateJSON = false;
|
70
63
|
|
71
64
|
// Backbone.Events
|
72
|
-
//
|
65
|
+
// ---------------
|
73
66
|
|
74
|
-
// Regular expression used to split event strings
|
67
|
+
// Regular expression used to split event strings.
|
75
68
|
var eventSplitter = /\s+/;
|
76
69
|
|
70
|
+
// Implement fancy features of the Events API such as multiple event
|
71
|
+
// names `"change blur"` and jQuery-style event maps `{change: action}`
|
72
|
+
// in terms of the existing API.
|
73
|
+
var eventsApi = function(obj, action, name, rest) {
|
74
|
+
if (!name) return true;
|
75
|
+
if (typeof name === 'object') {
|
76
|
+
for (var key in name) {
|
77
|
+
obj[action].apply(obj, [key, name[key]].concat(rest));
|
78
|
+
}
|
79
|
+
} else if (eventSplitter.test(name)) {
|
80
|
+
var names = name.split(eventSplitter);
|
81
|
+
for (var i = 0, l = names.length; i < l; i++) {
|
82
|
+
obj[action].apply(obj, [names[i]].concat(rest));
|
83
|
+
}
|
84
|
+
} else {
|
85
|
+
return true;
|
86
|
+
}
|
87
|
+
};
|
88
|
+
|
89
|
+
// Optimized internal dispatch function for triggering events. Tries to
|
90
|
+
// keep the usual cases speedy (most Backbone events have 3 arguments).
|
91
|
+
var triggerEvents = function(obj, events, args) {
|
92
|
+
var ev, i = -1, l = events.length;
|
93
|
+
switch (args.length) {
|
94
|
+
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
|
95
|
+
return;
|
96
|
+
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
|
97
|
+
return;
|
98
|
+
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
|
99
|
+
return;
|
100
|
+
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
|
101
|
+
return;
|
102
|
+
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
|
103
|
+
}
|
104
|
+
};
|
105
|
+
|
77
106
|
// A module that can be mixed in to *any object* in order to provide it with
|
78
|
-
// custom events. You may bind with `on` or remove with `off` callback
|
79
|
-
// to an event; trigger`-ing an event fires all callbacks in
|
107
|
+
// custom events. You may bind with `on` or remove with `off` callback
|
108
|
+
// functions to an event; `trigger`-ing an event fires all callbacks in
|
109
|
+
// succession.
|
80
110
|
//
|
81
111
|
// var object = {};
|
82
112
|
// _.extend(object, Backbone.Events);
|
@@ -85,58 +115,58 @@
|
|
85
115
|
//
|
86
116
|
var Events = Backbone.Events = {
|
87
117
|
|
88
|
-
// Bind one or more space separated events,
|
89
|
-
// function. Passing `"all"` will bind the callback to
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
// Create an immutable callback list, allowing traversal during
|
98
|
-
// modification. The tail is an empty object that will always be used
|
99
|
-
// as the next node.
|
100
|
-
while (event = events.shift()) {
|
101
|
-
list = calls[event];
|
102
|
-
node = list ? list.tail : {};
|
103
|
-
node.next = tail = {};
|
104
|
-
node.context = context;
|
105
|
-
node.callback = callback;
|
106
|
-
calls[event] = {tail: tail, next: list ? list.next : node};
|
107
|
-
}
|
108
|
-
|
118
|
+
// Bind one or more space separated events, or an events map,
|
119
|
+
// to a `callback` function. Passing `"all"` will bind the callback to
|
120
|
+
// all events fired.
|
121
|
+
on: function(name, callback, context) {
|
122
|
+
if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
|
123
|
+
this._events || (this._events = {});
|
124
|
+
var list = this._events[name] || (this._events[name] = []);
|
125
|
+
list.push({callback: callback, context: context, ctx: context || this});
|
109
126
|
return this;
|
110
127
|
},
|
111
128
|
|
112
|
-
//
|
113
|
-
//
|
114
|
-
|
115
|
-
|
116
|
-
var
|
129
|
+
// Bind events to only be triggered a single time. After the first time
|
130
|
+
// the callback is invoked, it will be removed.
|
131
|
+
once: function(name, callback, context) {
|
132
|
+
if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this;
|
133
|
+
var self = this;
|
134
|
+
var once = _.once(function() {
|
135
|
+
self.off(name, once);
|
136
|
+
callback.apply(this, arguments);
|
137
|
+
});
|
138
|
+
once._callback = callback;
|
139
|
+
this.on(name, once, context);
|
140
|
+
return this;
|
141
|
+
},
|
117
142
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
143
|
+
// Remove one or many callbacks. If `context` is null, removes all
|
144
|
+
// callbacks with that function. If `callback` is null, removes all
|
145
|
+
// callbacks for the event. If `events` is null, removes all bound
|
146
|
+
// callbacks for all events.
|
147
|
+
off: function(name, callback, context) {
|
148
|
+
var list, ev, events, names, i, l, j, k;
|
149
|
+
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
|
150
|
+
if (!name && !callback && !context) {
|
151
|
+
this._events = {};
|
122
152
|
return this;
|
123
153
|
}
|
124
154
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
this.on(event, cb, ctx);
|
155
|
+
names = name ? [name] : _.keys(this._events);
|
156
|
+
for (i = 0, l = names.length; i < l; i++) {
|
157
|
+
name = names[i];
|
158
|
+
if (list = this._events[name]) {
|
159
|
+
events = [];
|
160
|
+
if (callback || context) {
|
161
|
+
for (j = 0, k = list.length; j < k; j++) {
|
162
|
+
ev = list[j];
|
163
|
+
if ((callback && callback !== (ev.callback._callback || ev.callback)) ||
|
164
|
+
(context && context !== ev.context)) {
|
165
|
+
events.push(ev);
|
166
|
+
}
|
167
|
+
}
|
139
168
|
}
|
169
|
+
this._events[name] = events;
|
140
170
|
}
|
141
171
|
}
|
142
172
|
|
@@ -147,40 +177,53 @@
|
|
147
177
|
// passed the same arguments as `trigger` is, apart from the event name
|
148
178
|
// (unless you're listening on `"all"`, which will cause your callback to
|
149
179
|
// receive the true name of the event as the first argument).
|
150
|
-
trigger: function(
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
events =
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
180
|
+
trigger: function(name) {
|
181
|
+
if (!this._events) return this;
|
182
|
+
var args = slice.call(arguments, 1);
|
183
|
+
if (!eventsApi(this, 'trigger', name, args)) return this;
|
184
|
+
var events = this._events[name];
|
185
|
+
var allEvents = this._events.all;
|
186
|
+
if (events) triggerEvents(this, events, args);
|
187
|
+
if (allEvents) triggerEvents(this, allEvents, arguments);
|
188
|
+
return this;
|
189
|
+
},
|
190
|
+
|
191
|
+
// An inversion-of-control version of `on`. Tell *this* object to listen to
|
192
|
+
// an event in another object ... keeping track of what it's listening to.
|
193
|
+
listenTo: function(object, events, callback) {
|
194
|
+
var listeners = this._listeners || (this._listeners = {});
|
195
|
+
var id = object._listenerId || (object._listenerId = _.uniqueId('l'));
|
196
|
+
listeners[id] = object;
|
197
|
+
object.on(events, callback || this, this);
|
198
|
+
return this;
|
199
|
+
},
|
200
|
+
|
201
|
+
// Tell this object to stop listening to either specific events ... or
|
202
|
+
// to every object it's currently listening to.
|
203
|
+
stopListening: function(object, events, callback) {
|
204
|
+
var listeners = this._listeners;
|
205
|
+
if (!listeners) return;
|
206
|
+
if (object) {
|
207
|
+
object.off(events, callback, this);
|
208
|
+
if (!events && !callback) delete listeners[object._listenerId];
|
209
|
+
} else {
|
210
|
+
for (var id in listeners) {
|
211
|
+
listeners[id].off(null, null, this);
|
172
212
|
}
|
213
|
+
this._listeners = {};
|
173
214
|
}
|
174
|
-
|
175
215
|
return this;
|
176
216
|
}
|
177
|
-
|
178
217
|
};
|
179
218
|
|
180
219
|
// Aliases for backwards compatibility.
|
181
220
|
Events.bind = Events.on;
|
182
221
|
Events.unbind = Events.off;
|
183
222
|
|
223
|
+
// Allow the `Backbone` object to serve as a global event bus, for folks who
|
224
|
+
// want global "pubsub" in a convenient place.
|
225
|
+
_.extend(Backbone, Events);
|
226
|
+
|
184
227
|
// Backbone.Model
|
185
228
|
// --------------
|
186
229
|
|
@@ -188,23 +231,16 @@
|
|
188
231
|
// is automatically generated and assigned for you.
|
189
232
|
var Model = Backbone.Model = function(attributes, options) {
|
190
233
|
var defaults;
|
191
|
-
|
192
|
-
if (options && options.parse) attributes = this.parse(attributes);
|
193
|
-
if (defaults = getValue(this, 'defaults')) {
|
194
|
-
attributes = _.extend({}, defaults, attributes);
|
195
|
-
}
|
196
|
-
if (options && options.collection) this.collection = options.collection;
|
197
|
-
this.attributes = {};
|
198
|
-
this._escapedAttributes = {};
|
234
|
+
var attrs = attributes || {};
|
199
235
|
this.cid = _.uniqueId('c');
|
200
236
|
this.changed = {};
|
201
|
-
this.
|
202
|
-
this.
|
203
|
-
this.
|
204
|
-
|
205
|
-
this.
|
206
|
-
this.
|
207
|
-
this.
|
237
|
+
this.attributes = {};
|
238
|
+
this._changes = [];
|
239
|
+
if (options && options.collection) this.collection = options.collection;
|
240
|
+
if (options && options.parse) attrs = this.parse(attrs);
|
241
|
+
if (defaults = _.result(this, 'defaults')) _.defaults(attrs, defaults);
|
242
|
+
this.set(attrs, {silent: true});
|
243
|
+
this._currentAttributes = _.clone(this.attributes);
|
208
244
|
this._previousAttributes = _.clone(this.attributes);
|
209
245
|
this.initialize.apply(this, arguments);
|
210
246
|
};
|
@@ -215,14 +251,6 @@
|
|
215
251
|
// A hash of attributes whose current and previous value differ.
|
216
252
|
changed: null,
|
217
253
|
|
218
|
-
// A hash of attributes that have silently changed since the last time
|
219
|
-
// `change` was called. Will become pending attributes on the next call.
|
220
|
-
_silent: null,
|
221
|
-
|
222
|
-
// A hash of attributes that have changed since the last `'change'` event
|
223
|
-
// began.
|
224
|
-
_pending: null,
|
225
|
-
|
226
254
|
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
227
255
|
// CouchDB users may want to set this to `"_id"`.
|
228
256
|
idAttribute: 'id',
|
@@ -236,6 +264,11 @@
|
|
236
264
|
return _.clone(this.attributes);
|
237
265
|
},
|
238
266
|
|
267
|
+
// Proxy `Backbone.sync` by default.
|
268
|
+
sync: function() {
|
269
|
+
return Backbone.sync.apply(this, arguments);
|
270
|
+
},
|
271
|
+
|
239
272
|
// Get the value of an attribute.
|
240
273
|
get: function(attr) {
|
241
274
|
return this.attributes[attr];
|
@@ -243,10 +276,7 @@
|
|
243
276
|
|
244
277
|
// Get the HTML-escaped value of an attribute.
|
245
278
|
escape: function(attr) {
|
246
|
-
|
247
|
-
if (html = this._escapedAttributes[attr]) return html;
|
248
|
-
var val = this.get(attr);
|
249
|
-
return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
|
279
|
+
return _.escape(this.get(attr));
|
250
280
|
},
|
251
281
|
|
252
282
|
// Returns `true` if the attribute contains a value that is not null
|
@@ -257,23 +287,21 @@
|
|
257
287
|
|
258
288
|
// Set a hash of model attributes on the object, firing `"change"` unless
|
259
289
|
// you choose to silence it.
|
260
|
-
set: function(key,
|
261
|
-
var
|
290
|
+
set: function(key, val, options) {
|
291
|
+
var attr, attrs;
|
292
|
+
if (key == null) return this;
|
262
293
|
|
263
294
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
264
|
-
if (_.isObject(key)
|
295
|
+
if (_.isObject(key)) {
|
265
296
|
attrs = key;
|
266
|
-
options =
|
297
|
+
options = val;
|
267
298
|
} else {
|
268
|
-
attrs = {};
|
269
|
-
attrs[key] = value;
|
299
|
+
(attrs = {})[key] = val;
|
270
300
|
}
|
271
301
|
|
272
302
|
// Extract attributes and options.
|
273
|
-
|
274
|
-
|
275
|
-
if (attrs instanceof Model) attrs = attrs.attributes;
|
276
|
-
if (options.unset) for (attr in attrs) attrs[attr] = void 0;
|
303
|
+
var silent = options && options.silent;
|
304
|
+
var unset = options && options.unset;
|
277
305
|
|
278
306
|
// Run validation.
|
279
307
|
if (!this._validate(attrs, options)) return false;
|
@@ -281,52 +309,38 @@
|
|
281
309
|
// Check for changes of `id`.
|
282
310
|
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
283
311
|
|
284
|
-
var changes = options.changes = {};
|
285
312
|
var now = this.attributes;
|
286
|
-
var escaped = this._escapedAttributes;
|
287
|
-
var prev = this._previousAttributes || {};
|
288
313
|
|
289
314
|
// For each `set` attribute...
|
290
315
|
for (attr in attrs) {
|
291
316
|
val = attrs[attr];
|
292
317
|
|
293
|
-
//
|
294
|
-
|
295
|
-
|
296
|
-
(options.silent ? this._silent : changes)[attr] = true;
|
297
|
-
}
|
298
|
-
|
299
|
-
// Update or delete the current value.
|
300
|
-
options.unset ? delete now[attr] : now[attr] = val;
|
301
|
-
|
302
|
-
// If the new and previous value differ, record the change. If not,
|
303
|
-
// then remove changes for this attribute.
|
304
|
-
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
|
305
|
-
this.changed[attr] = val;
|
306
|
-
if (!options.silent) this._pending[attr] = true;
|
307
|
-
} else {
|
308
|
-
delete this.changed[attr];
|
309
|
-
delete this._pending[attr];
|
310
|
-
}
|
318
|
+
// Update or delete the current value, and track the change.
|
319
|
+
unset ? delete now[attr] : now[attr] = val;
|
320
|
+
this._changes.push(attr, val);
|
311
321
|
}
|
312
322
|
|
323
|
+
// Signal that the model's state has potentially changed, and we need
|
324
|
+
// to recompute the actual changes.
|
325
|
+
this._hasComputed = false;
|
326
|
+
|
313
327
|
// Fire the `"change"` events.
|
314
|
-
if (!
|
328
|
+
if (!silent) this.change(options);
|
315
329
|
return this;
|
316
330
|
},
|
317
331
|
|
318
332
|
// Remove an attribute from the model, firing `"change"` unless you choose
|
319
333
|
// to silence it. `unset` is a noop if the attribute doesn't exist.
|
320
334
|
unset: function(attr, options) {
|
321
|
-
(
|
322
|
-
return this.set(attr, null, options);
|
335
|
+
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
|
323
336
|
},
|
324
337
|
|
325
338
|
// Clear all attributes on the model, firing `"change"` unless you choose
|
326
339
|
// to silence it.
|
327
340
|
clear: function(options) {
|
328
|
-
|
329
|
-
|
341
|
+
var attrs = {};
|
342
|
+
for (var key in this.attributes) attrs[key] = void 0;
|
343
|
+
return this.set(attrs, _.extend({}, options, {unset: true}));
|
330
344
|
},
|
331
345
|
|
332
346
|
// Fetch the model from the server. If the server's representation of the
|
@@ -334,35 +348,34 @@
|
|
334
348
|
// triggering a `"change"` event.
|
335
349
|
fetch: function(options) {
|
336
350
|
options = options ? _.clone(options) : {};
|
351
|
+
if (options.parse === void 0) options.parse = true;
|
337
352
|
var model = this;
|
338
353
|
var success = options.success;
|
339
354
|
options.success = function(resp, status, xhr) {
|
340
|
-
if (!model.set(model.parse(resp
|
341
|
-
if (success) success(model, resp);
|
355
|
+
if (!model.set(model.parse(resp), options)) return false;
|
356
|
+
if (success) success(model, resp, options);
|
342
357
|
};
|
343
|
-
|
344
|
-
return (this.sync || Backbone.sync).call(this, 'read', this, options);
|
358
|
+
return this.sync('read', this, options);
|
345
359
|
},
|
346
360
|
|
347
361
|
// Set a hash of model attributes, and sync the model to the server.
|
348
362
|
// If the server returns an attributes hash that differs, the model's
|
349
363
|
// state will be `set` again.
|
350
|
-
save: function(key,
|
351
|
-
var attrs, current;
|
364
|
+
save: function(key, val, options) {
|
365
|
+
var attrs, current, done;
|
352
366
|
|
353
|
-
// Handle both `
|
354
|
-
if (_.isObject(key)
|
367
|
+
// Handle both `"key", value` and `{key: value}` -style arguments.
|
368
|
+
if (key == null || _.isObject(key)) {
|
355
369
|
attrs = key;
|
356
|
-
options =
|
357
|
-
} else {
|
358
|
-
attrs = {};
|
359
|
-
attrs[key] = value;
|
370
|
+
options = val;
|
371
|
+
} else if (key != null) {
|
372
|
+
(attrs = {})[key] = val;
|
360
373
|
}
|
361
374
|
options = options ? _.clone(options) : {};
|
362
375
|
|
363
376
|
// If we're "wait"-ing to set changed attributes, validate early.
|
364
377
|
if (options.wait) {
|
365
|
-
if (!this._validate(attrs, options)) return false;
|
378
|
+
if (attrs && !this._validate(attrs, options)) return false;
|
366
379
|
current = _.clone(this.attributes);
|
367
380
|
}
|
368
381
|
|
@@ -372,29 +385,33 @@
|
|
372
385
|
return false;
|
373
386
|
}
|
374
387
|
|
388
|
+
// Do not persist invalid models.
|
389
|
+
if (!attrs && !this._validate(null, options)) return false;
|
390
|
+
|
375
391
|
// After a successful server-side save, the client is (optionally)
|
376
392
|
// updated with the server-side state.
|
377
393
|
var model = this;
|
378
394
|
var success = options.success;
|
379
395
|
options.success = function(resp, status, xhr) {
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
serverAttrs = _.extend(attrs || {}, serverAttrs);
|
384
|
-
}
|
396
|
+
done = true;
|
397
|
+
var serverAttrs = model.parse(resp);
|
398
|
+
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
|
385
399
|
if (!model.set(serverAttrs, options)) return false;
|
386
|
-
if (success)
|
387
|
-
success(model, resp);
|
388
|
-
} else {
|
389
|
-
model.trigger('sync', model, resp, options);
|
390
|
-
}
|
400
|
+
if (success) success(model, resp, options);
|
391
401
|
};
|
392
402
|
|
393
403
|
// Finish configuring and sending the Ajax request.
|
394
|
-
|
395
|
-
|
396
|
-
var xhr =
|
397
|
-
|
404
|
+
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
|
405
|
+
if (method == 'patch') options.attrs = attrs;
|
406
|
+
var xhr = this.sync(method, this, options);
|
407
|
+
|
408
|
+
// When using `wait`, reset attributes to original values unless
|
409
|
+
// `success` has been called already.
|
410
|
+
if (!done && options.wait) {
|
411
|
+
this.clear(silentOptions);
|
412
|
+
this.set(current, silentOptions);
|
413
|
+
}
|
414
|
+
|
398
415
|
return xhr;
|
399
416
|
},
|
400
417
|
|
@@ -406,27 +423,22 @@
|
|
406
423
|
var model = this;
|
407
424
|
var success = options.success;
|
408
425
|
|
409
|
-
var
|
426
|
+
var destroy = function() {
|
410
427
|
model.trigger('destroy', model, model.collection, options);
|
411
428
|
};
|
412
429
|
|
430
|
+
options.success = function(resp) {
|
431
|
+
if (options.wait || model.isNew()) destroy();
|
432
|
+
if (success) success(model, resp, options);
|
433
|
+
};
|
434
|
+
|
413
435
|
if (this.isNew()) {
|
414
|
-
|
436
|
+
options.success();
|
415
437
|
return false;
|
416
438
|
}
|
417
439
|
|
418
|
-
|
419
|
-
|
420
|
-
if (success) {
|
421
|
-
success(model, resp);
|
422
|
-
} else {
|
423
|
-
model.trigger('sync', model, resp, options);
|
424
|
-
}
|
425
|
-
};
|
426
|
-
|
427
|
-
options.error = Backbone.wrapError(options.error, model, options);
|
428
|
-
var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
|
429
|
-
if (!options.wait) triggerDestroy();
|
440
|
+
var xhr = this.sync('delete', this, options);
|
441
|
+
if (!options.wait) destroy();
|
430
442
|
return xhr;
|
431
443
|
},
|
432
444
|
|
@@ -434,14 +446,14 @@
|
|
434
446
|
// using Backbone's restful methods, override this to change the endpoint
|
435
447
|
// that will be called.
|
436
448
|
url: function() {
|
437
|
-
var base =
|
449
|
+
var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
|
438
450
|
if (this.isNew()) return base;
|
439
|
-
return base + (base.charAt(base.length - 1)
|
451
|
+
return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
|
440
452
|
},
|
441
453
|
|
442
454
|
// **parse** converts a response into the hash of attributes to be `set` on
|
443
455
|
// the model. The default implementation is just to pass the response along.
|
444
|
-
parse: function(resp
|
456
|
+
parse: function(resp) {
|
445
457
|
return resp;
|
446
458
|
},
|
447
459
|
|
@@ -459,30 +471,24 @@
|
|
459
471
|
// a `"change:attribute"` event for each changed attribute.
|
460
472
|
// Calling this will cause all objects observing the model to update.
|
461
473
|
change: function(options) {
|
462
|
-
options || (options = {});
|
463
474
|
var changing = this._changing;
|
464
475
|
this._changing = true;
|
465
476
|
|
466
|
-
//
|
467
|
-
|
477
|
+
// Generate the changes to be triggered on the model.
|
478
|
+
var triggers = this._computeChanges(true);
|
468
479
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
this.trigger('change:' + attr, this, this.get(attr), options);
|
480
|
+
this._pending = !!triggers.length;
|
481
|
+
|
482
|
+
for (var i = triggers.length - 2; i >= 0; i -= 2) {
|
483
|
+
this.trigger('change:' + triggers[i], this, triggers[i + 1], options);
|
474
484
|
}
|
485
|
+
|
475
486
|
if (changing) return this;
|
476
487
|
|
477
|
-
//
|
478
|
-
while (
|
479
|
-
this._pending =
|
488
|
+
// Trigger a `change` while there have been changes.
|
489
|
+
while (this._pending) {
|
490
|
+
this._pending = false;
|
480
491
|
this.trigger('change', this, options);
|
481
|
-
// Pending and silent changes still remain.
|
482
|
-
for (var attr in this.changed) {
|
483
|
-
if (this._pending[attr] || this._silent[attr]) continue;
|
484
|
-
delete this.changed[attr];
|
485
|
-
}
|
486
492
|
this._previousAttributes = _.clone(this.attributes);
|
487
493
|
}
|
488
494
|
|
@@ -493,7 +499,8 @@
|
|
493
499
|
// Determine if the model has changed since the last `"change"` event.
|
494
500
|
// If you specify an attribute name, determine if that attribute has changed.
|
495
501
|
hasChanged: function(attr) {
|
496
|
-
if (!
|
502
|
+
if (!this._hasComputed) this._computeChanges();
|
503
|
+
if (attr == null) return !_.isEmpty(this.changed);
|
497
504
|
return _.has(this.changed, attr);
|
498
505
|
},
|
499
506
|
|
@@ -513,10 +520,43 @@
|
|
513
520
|
return changed;
|
514
521
|
},
|
515
522
|
|
523
|
+
// Looking at the built up list of `set` attribute changes, compute how
|
524
|
+
// many of the attributes have actually changed. If `loud`, return a
|
525
|
+
// boiled-down list of only the real changes.
|
526
|
+
_computeChanges: function(loud) {
|
527
|
+
this.changed = {};
|
528
|
+
var already = {};
|
529
|
+
var triggers = [];
|
530
|
+
var current = this._currentAttributes;
|
531
|
+
var changes = this._changes;
|
532
|
+
|
533
|
+
// Loop through the current queue of potential model changes.
|
534
|
+
for (var i = changes.length - 2; i >= 0; i -= 2) {
|
535
|
+
var key = changes[i], val = changes[i + 1];
|
536
|
+
if (already[key]) continue;
|
537
|
+
already[key] = true;
|
538
|
+
|
539
|
+
// Check if the attribute has been modified since the last change,
|
540
|
+
// and update `this.changed` accordingly. If we're inside of a `change`
|
541
|
+
// call, also add a trigger to the list.
|
542
|
+
if (current[key] !== val) {
|
543
|
+
this.changed[key] = val;
|
544
|
+
if (!loud) continue;
|
545
|
+
triggers.push(key, val);
|
546
|
+
current[key] = val;
|
547
|
+
}
|
548
|
+
}
|
549
|
+
if (loud) this._changes = [];
|
550
|
+
|
551
|
+
// Signals `this.changed` is current to prevent duplicate calls from `this.hasChanged`.
|
552
|
+
this._hasComputed = true;
|
553
|
+
return triggers;
|
554
|
+
},
|
555
|
+
|
516
556
|
// Get the previous value of an attribute, recorded at the time the last
|
517
557
|
// `"change"` event was fired.
|
518
558
|
previous: function(attr) {
|
519
|
-
if (
|
559
|
+
if (attr == null || !this._previousAttributes) return null;
|
520
560
|
return this._previousAttributes[attr];
|
521
561
|
},
|
522
562
|
|
@@ -526,25 +566,16 @@
|
|
526
566
|
return _.clone(this._previousAttributes);
|
527
567
|
},
|
528
568
|
|
529
|
-
// Check if the model is currently in a valid state. It's only possible to
|
530
|
-
// get into an *invalid* state if you're using silent changes.
|
531
|
-
isValid: function() {
|
532
|
-
return !this.validate(this.attributes);
|
533
|
-
},
|
534
|
-
|
535
569
|
// Run validation against the next complete set of model attributes,
|
536
570
|
// returning `true` if all is well. If a specific `error` callback has
|
537
571
|
// been passed, call that instead of firing the general `"error"` event.
|
538
572
|
_validate: function(attrs, options) {
|
539
|
-
if (
|
573
|
+
if (!this.validate) return true;
|
540
574
|
attrs = _.extend({}, this.attributes, attrs);
|
541
575
|
var error = this.validate(attrs, options);
|
542
576
|
if (!error) return true;
|
543
|
-
if (options && options.error)
|
544
|
-
|
545
|
-
} else {
|
546
|
-
this.trigger('error', this, error, options);
|
547
|
-
}
|
577
|
+
if (options && options.error) options.error(this, error, options);
|
578
|
+
this.trigger('error', this, error, options);
|
548
579
|
return false;
|
549
580
|
}
|
550
581
|
|
@@ -559,10 +590,10 @@
|
|
559
590
|
var Collection = Backbone.Collection = function(models, options) {
|
560
591
|
options || (options = {});
|
561
592
|
if (options.model) this.model = options.model;
|
562
|
-
if (options.comparator) this.comparator = options.comparator;
|
593
|
+
if (options.comparator !== void 0) this.comparator = options.comparator;
|
563
594
|
this._reset();
|
564
595
|
this.initialize.apply(this, arguments);
|
565
|
-
if (models) this.reset(models, {silent: true,
|
596
|
+
if (models) this.reset(models, _.extend({silent: true}, options));
|
566
597
|
};
|
567
598
|
|
568
599
|
// Define the Collection's inheritable methods.
|
@@ -582,54 +613,65 @@
|
|
582
613
|
return this.map(function(model){ return model.toJSON(options); });
|
583
614
|
},
|
584
615
|
|
616
|
+
// Proxy `Backbone.sync` by default.
|
617
|
+
sync: function() {
|
618
|
+
return Backbone.sync.apply(this, arguments);
|
619
|
+
},
|
620
|
+
|
585
621
|
// Add a model, or list of models to the set. Pass **silent** to avoid
|
586
622
|
// firing the `add` event for every new model.
|
587
623
|
add: function(models, options) {
|
588
|
-
var i,
|
589
|
-
|
624
|
+
var i, args, length, model, existing, needsSort;
|
625
|
+
var at = options && options.at;
|
626
|
+
var sort = ((options && options.sort) == null ? true : options.sort);
|
590
627
|
models = _.isArray(models) ? models.slice() : [models];
|
591
628
|
|
592
|
-
//
|
593
|
-
//
|
594
|
-
for (i =
|
595
|
-
if
|
596
|
-
|
629
|
+
// Turn bare objects into model references, and prevent invalid models
|
630
|
+
// from being added.
|
631
|
+
for (i = models.length - 1; i >= 0; i--) {
|
632
|
+
if(!(model = this._prepareModel(models[i], options))) {
|
633
|
+
this.trigger("error", this, models[i], options);
|
634
|
+
models.splice(i, 1);
|
635
|
+
continue;
|
597
636
|
}
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
637
|
+
models[i] = model;
|
638
|
+
|
639
|
+
existing = model.id != null && this._byId[model.id];
|
640
|
+
// If a duplicate is found, prevent it from being added and
|
641
|
+
// optionally merge it into the existing model.
|
642
|
+
if (existing || this._byCid[model.cid]) {
|
643
|
+
if (options && options.merge && existing) {
|
644
|
+
existing.set(model.attributes, options);
|
645
|
+
needsSort = sort;
|
646
|
+
}
|
647
|
+
models.splice(i, 1);
|
602
648
|
continue;
|
603
649
|
}
|
604
|
-
cids[cid] = ids[id] = model;
|
605
|
-
}
|
606
650
|
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
models.splice(dups[i], 1);
|
611
|
-
}
|
612
|
-
|
613
|
-
// Listen to added models' events, and index models for lookup by
|
614
|
-
// `id` and by `cid`.
|
615
|
-
for (i = 0, length = models.length; i < length; i++) {
|
616
|
-
(model = models[i]).on('all', this._onModelEvent, this);
|
651
|
+
// Listen to added models' events, and index models for lookup by
|
652
|
+
// `id` and by `cid`.
|
653
|
+
model.on('all', this._onModelEvent, this);
|
617
654
|
this._byCid[model.cid] = model;
|
618
655
|
if (model.id != null) this._byId[model.id] = model;
|
619
656
|
}
|
620
657
|
|
621
|
-
//
|
622
|
-
|
623
|
-
this.length += length;
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
658
|
+
// See if sorting is needed, update `length` and splice in new models.
|
659
|
+
if (models.length) needsSort = sort;
|
660
|
+
this.length += models.length;
|
661
|
+
args = [at != null ? at : this.models.length, 0];
|
662
|
+
push.apply(args, models);
|
663
|
+
splice.apply(this.models, args);
|
664
|
+
|
665
|
+
// Sort the collection if appropriate.
|
666
|
+
if (needsSort && this.comparator && at == null) this.sort({silent: true});
|
667
|
+
|
668
|
+
if (options && options.silent) return this;
|
669
|
+
|
670
|
+
// Trigger `add` events.
|
671
|
+
while (model = models.shift()) {
|
631
672
|
model.trigger('add', model, this, options);
|
632
673
|
}
|
674
|
+
|
633
675
|
return this;
|
634
676
|
},
|
635
677
|
|
@@ -640,7 +682,7 @@
|
|
640
682
|
options || (options = {});
|
641
683
|
models = _.isArray(models) ? models.slice() : [models];
|
642
684
|
for (i = 0, l = models.length; i < l; i++) {
|
643
|
-
model = this.
|
685
|
+
model = this.get(models[i]);
|
644
686
|
if (!model) continue;
|
645
687
|
delete this._byId[model.id];
|
646
688
|
delete this._byCid[model.cid];
|
@@ -659,7 +701,7 @@
|
|
659
701
|
// Add a model to the end of the collection.
|
660
702
|
push: function(model, options) {
|
661
703
|
model = this._prepareModel(model, options);
|
662
|
-
this.add(model, options);
|
704
|
+
this.add(model, _.extend({at: this.length}, options));
|
663
705
|
return model;
|
664
706
|
},
|
665
707
|
|
@@ -684,15 +726,15 @@
|
|
684
726
|
return model;
|
685
727
|
},
|
686
728
|
|
687
|
-
//
|
688
|
-
|
689
|
-
|
690
|
-
return this._byId[id.id != null ? id.id : id];
|
729
|
+
// Slice out a sub-array of models from the collection.
|
730
|
+
slice: function(begin, end) {
|
731
|
+
return this.models.slice(begin, end);
|
691
732
|
},
|
692
733
|
|
693
|
-
// Get a model from the set by
|
694
|
-
|
695
|
-
|
734
|
+
// Get a model from the set by id.
|
735
|
+
get: function(obj) {
|
736
|
+
if (obj == null) return void 0;
|
737
|
+
return this._byId[obj.id != null ? obj.id : obj] || this._byCid[obj.cid || obj];
|
696
738
|
},
|
697
739
|
|
698
740
|
// Get the model at the given index.
|
@@ -715,34 +757,74 @@
|
|
715
757
|
// normal circumstances, as the set will maintain sort order as each item
|
716
758
|
// is added.
|
717
759
|
sort: function(options) {
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
760
|
+
if (!this.comparator) {
|
761
|
+
throw new Error('Cannot sort a set without a comparator');
|
762
|
+
}
|
763
|
+
|
764
|
+
if (_.isString(this.comparator) || this.comparator.length === 1) {
|
765
|
+
this.models = this.sortBy(this.comparator, this);
|
723
766
|
} else {
|
724
|
-
this.models.sort(
|
767
|
+
this.models.sort(_.bind(this.comparator, this));
|
725
768
|
}
|
726
|
-
|
769
|
+
|
770
|
+
if (!options || !options.silent) this.trigger('sort', this, options);
|
727
771
|
return this;
|
728
772
|
},
|
729
773
|
|
730
774
|
// Pluck an attribute from each model in the collection.
|
731
775
|
pluck: function(attr) {
|
732
|
-
return _.
|
776
|
+
return _.invoke(this.models, 'get', attr);
|
777
|
+
},
|
778
|
+
|
779
|
+
// Smartly update a collection with a change set of models, adding,
|
780
|
+
// removing, and merging as necessary.
|
781
|
+
update: function(models, options) {
|
782
|
+
var model, i, l, existing;
|
783
|
+
var add = [], remove = [], modelMap = {};
|
784
|
+
var idAttr = this.model.prototype.idAttribute;
|
785
|
+
options = _.extend({add: true, merge: true, remove: true}, options);
|
786
|
+
if (options.parse) models = this.parse(models);
|
787
|
+
|
788
|
+
// Allow a single model (or no argument) to be passed.
|
789
|
+
if (!_.isArray(models)) models = models ? [models] : [];
|
790
|
+
|
791
|
+
// Proxy to `add` for this case, no need to iterate...
|
792
|
+
if (options.add && !options.remove) return this.add(models, options);
|
793
|
+
|
794
|
+
// Determine which models to add and merge, and which to remove.
|
795
|
+
for (i = 0, l = models.length; i < l; i++) {
|
796
|
+
model = models[i];
|
797
|
+
existing = this.get(model.id || model.cid || model[idAttr]);
|
798
|
+
if (options.remove && existing) modelMap[existing.cid] = true;
|
799
|
+
if ((options.add && !existing) || (options.merge && existing)) {
|
800
|
+
add.push(model);
|
801
|
+
}
|
802
|
+
}
|
803
|
+
if (options.remove) {
|
804
|
+
for (i = 0, l = this.models.length; i < l; i++) {
|
805
|
+
model = this.models[i];
|
806
|
+
if (!modelMap[model.cid]) remove.push(model);
|
807
|
+
}
|
808
|
+
}
|
809
|
+
|
810
|
+
// Remove models (if applicable) before we add and merge the rest.
|
811
|
+
if (remove.length) this.remove(remove, options);
|
812
|
+
if (add.length) this.add(add, options);
|
813
|
+
return this;
|
733
814
|
},
|
734
815
|
|
735
816
|
// When you have more items than you want to add or remove individually,
|
736
817
|
// you can reset the entire set with a new list of models, without firing
|
737
818
|
// any `add` or `remove` events. Fires `reset` when finished.
|
738
819
|
reset: function(models, options) {
|
739
|
-
models || (models = []);
|
740
820
|
options || (options = {});
|
821
|
+
if (options.parse) models = this.parse(models);
|
741
822
|
for (var i = 0, l = this.models.length; i < l; i++) {
|
742
823
|
this._removeReference(this.models[i]);
|
743
824
|
}
|
825
|
+
options.previousModels = this.models;
|
744
826
|
this._reset();
|
745
|
-
this.add(models, _.extend({silent: true}, options));
|
827
|
+
if (models) this.add(models, _.extend({silent: true}, options));
|
746
828
|
if (!options.silent) this.trigger('reset', this, options);
|
747
829
|
return this;
|
748
830
|
},
|
@@ -752,34 +834,30 @@
|
|
752
834
|
// models to the collection instead of resetting.
|
753
835
|
fetch: function(options) {
|
754
836
|
options = options ? _.clone(options) : {};
|
755
|
-
if (options.parse ===
|
837
|
+
if (options.parse === void 0) options.parse = true;
|
756
838
|
var collection = this;
|
757
839
|
var success = options.success;
|
758
840
|
options.success = function(resp, status, xhr) {
|
759
|
-
|
760
|
-
|
841
|
+
var method = options.update ? 'update' : 'reset';
|
842
|
+
collection[method](resp, options);
|
843
|
+
if (success) success(collection, resp, options);
|
761
844
|
};
|
762
|
-
|
763
|
-
return (this.sync || Backbone.sync).call(this, 'read', this, options);
|
845
|
+
return this.sync('read', this, options);
|
764
846
|
},
|
765
847
|
|
766
848
|
// Create a new instance of a model in this collection. Add the model to the
|
767
849
|
// collection immediately, unless `wait: true` is passed, in which case we
|
768
850
|
// wait for the server to agree.
|
769
851
|
create: function(model, options) {
|
770
|
-
var
|
852
|
+
var collection = this;
|
771
853
|
options = options ? _.clone(options) : {};
|
772
854
|
model = this._prepareModel(model, options);
|
773
855
|
if (!model) return false;
|
774
|
-
if (!options.wait)
|
856
|
+
if (!options.wait) collection.add(model, options);
|
775
857
|
var success = options.success;
|
776
|
-
options.success = function(
|
777
|
-
if (options.wait)
|
778
|
-
if (success)
|
779
|
-
success(nextModel, resp);
|
780
|
-
} else {
|
781
|
-
nextModel.trigger('sync', model, resp, options);
|
782
|
-
}
|
858
|
+
options.success = function(model, resp, options) {
|
859
|
+
if (options.wait) collection.add(model, options);
|
860
|
+
if (success) success(model, resp, options);
|
783
861
|
};
|
784
862
|
model.save(null, options);
|
785
863
|
return model;
|
@@ -787,19 +865,24 @@
|
|
787
865
|
|
788
866
|
// **parse** converts a response into a list of models to be added to the
|
789
867
|
// collection. The default implementation is just to pass it through.
|
790
|
-
parse: function(resp
|
868
|
+
parse: function(resp) {
|
791
869
|
return resp;
|
792
870
|
},
|
793
871
|
|
872
|
+
// Create a new collection with an identical list of models as this one.
|
873
|
+
clone: function() {
|
874
|
+
return new this.constructor(this.models);
|
875
|
+
},
|
876
|
+
|
794
877
|
// Proxy to _'s chain. Can't be proxied the same way the rest of the
|
795
878
|
// underscore methods are proxied because it relies on the underscore
|
796
879
|
// constructor.
|
797
|
-
chain: function
|
880
|
+
chain: function() {
|
798
881
|
return _(this.models).chain();
|
799
882
|
},
|
800
883
|
|
801
884
|
// Reset all internal state. Called when the collection is reset.
|
802
|
-
_reset: function(
|
885
|
+
_reset: function() {
|
803
886
|
this.length = 0;
|
804
887
|
this.models = [];
|
805
888
|
this._byId = {};
|
@@ -807,24 +890,21 @@
|
|
807
890
|
},
|
808
891
|
|
809
892
|
// Prepare a model or hash of attributes to be added to this collection.
|
810
|
-
_prepareModel: function(
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
options.collection = this;
|
815
|
-
model = new this.model(attrs, options);
|
816
|
-
if (!model._validate(model.attributes, options)) model = false;
|
817
|
-
} else if (!model.collection) {
|
818
|
-
model.collection = this;
|
893
|
+
_prepareModel: function(attrs, options) {
|
894
|
+
if (attrs instanceof Model) {
|
895
|
+
if (!attrs.collection) attrs.collection = this;
|
896
|
+
return attrs;
|
819
897
|
}
|
898
|
+
options || (options = {});
|
899
|
+
options.collection = this;
|
900
|
+
var model = new this.model(attrs, options);
|
901
|
+
if (!model._validate(attrs, options)) return false;
|
820
902
|
return model;
|
821
903
|
},
|
822
904
|
|
823
905
|
// Internal method to remove a model's ties to a collection.
|
824
906
|
_removeReference: function(model) {
|
825
|
-
if (this
|
826
|
-
delete model.collection;
|
827
|
-
}
|
907
|
+
if (this === model.collection) delete model.collection;
|
828
908
|
model.off('all', this._onModelEvent, this);
|
829
909
|
},
|
830
910
|
|
@@ -833,13 +913,11 @@
|
|
833
913
|
// events simply proxy through. "add" and "remove" events that originate
|
834
914
|
// in other collections are ignored.
|
835
915
|
_onModelEvent: function(event, model, collection, options) {
|
836
|
-
if ((event
|
837
|
-
if (event
|
838
|
-
this.remove(model, options);
|
839
|
-
}
|
916
|
+
if ((event === 'add' || event === 'remove') && collection !== this) return;
|
917
|
+
if (event === 'destroy') this.remove(model, options);
|
840
918
|
if (model && event === 'change:' + model.idAttribute) {
|
841
919
|
delete this._byId[model.previous(model.idAttribute)];
|
842
|
-
this._byId[model.id] = model;
|
920
|
+
if (model.id != null) this._byId[model.id] = model;
|
843
921
|
}
|
844
922
|
this.trigger.apply(this, arguments);
|
845
923
|
}
|
@@ -847,21 +925,37 @@
|
|
847
925
|
});
|
848
926
|
|
849
927
|
// Underscore methods that we want to implement on the Collection.
|
850
|
-
var methods = ['forEach', 'each', 'map', '
|
851
|
-
'
|
852
|
-
'
|
853
|
-
'
|
854
|
-
'
|
928
|
+
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
|
929
|
+
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
|
930
|
+
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
|
931
|
+
'max', 'min', 'sortedIndex', 'toArray', 'size', 'first', 'head', 'take',
|
932
|
+
'initial', 'rest', 'tail', 'last', 'without', 'indexOf', 'shuffle',
|
933
|
+
'lastIndexOf', 'isEmpty'];
|
855
934
|
|
856
935
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
857
936
|
_.each(methods, function(method) {
|
858
937
|
Collection.prototype[method] = function() {
|
859
|
-
|
938
|
+
var args = slice.call(arguments);
|
939
|
+
args.unshift(this.models);
|
940
|
+
return _[method].apply(_, args);
|
941
|
+
};
|
942
|
+
});
|
943
|
+
|
944
|
+
// Underscore methods that take a property name as an argument.
|
945
|
+
var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
|
946
|
+
|
947
|
+
// Use attributes instead of properties.
|
948
|
+
_.each(attributeMethods, function(method) {
|
949
|
+
Collection.prototype[method] = function(value, context) {
|
950
|
+
var iterator = _.isFunction(value) ? value : function(model) {
|
951
|
+
return model.get(value);
|
952
|
+
};
|
953
|
+
return _[method](this.models, iterator, context);
|
860
954
|
};
|
861
955
|
});
|
862
956
|
|
863
957
|
// Backbone.Router
|
864
|
-
//
|
958
|
+
// ---------------
|
865
959
|
|
866
960
|
// Routers map faux-URLs to actions, and fire events when routes are
|
867
961
|
// matched. Creating a new one sets its `routes` hash, if not set statically.
|
@@ -874,9 +968,10 @@
|
|
874
968
|
|
875
969
|
// Cached regular expressions for matching named param parts and splatted
|
876
970
|
// parts of route strings.
|
971
|
+
var optionalParam = /\((.*?)\)/g;
|
877
972
|
var namedParam = /:\w+/g;
|
878
973
|
var splatParam = /\*\w+/g;
|
879
|
-
var escapeRegExp = /[
|
974
|
+
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
|
880
975
|
|
881
976
|
// Set up all inheritable **Backbone.Router** properties and methods.
|
882
977
|
_.extend(Router.prototype, Events, {
|
@@ -892,7 +987,6 @@
|
|
892
987
|
// });
|
893
988
|
//
|
894
989
|
route: function(route, name, callback) {
|
895
|
-
Backbone.history || (Backbone.history = new History);
|
896
990
|
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
897
991
|
if (!callback) callback = this[name];
|
898
992
|
Backbone.history.route(route, _.bind(function(fragment) {
|
@@ -907,6 +1001,7 @@
|
|
907
1001
|
// Simple proxy to `Backbone.history` to save a fragment into the history.
|
908
1002
|
navigate: function(fragment, options) {
|
909
1003
|
Backbone.history.navigate(fragment, options);
|
1004
|
+
return this;
|
910
1005
|
},
|
911
1006
|
|
912
1007
|
// Bind all defined routes to `Backbone.history`. We have to reverse the
|
@@ -914,12 +1009,9 @@
|
|
914
1009
|
// routes can be defined at the bottom of the route map.
|
915
1010
|
_bindRoutes: function() {
|
916
1011
|
if (!this.routes) return;
|
917
|
-
var routes =
|
918
|
-
|
919
|
-
|
920
|
-
}
|
921
|
-
for (var i = 0, l = routes.length; i < l; i++) {
|
922
|
-
this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
|
1012
|
+
var route, routes = _.keys(this.routes);
|
1013
|
+
while ((route = routes.pop()) != null) {
|
1014
|
+
this.route(route, this.routes[route]);
|
923
1015
|
}
|
924
1016
|
},
|
925
1017
|
|
@@ -927,6 +1019,7 @@
|
|
927
1019
|
// against the current location hash.
|
928
1020
|
_routeToRegExp: function(route) {
|
929
1021
|
route = route.replace(escapeRegExp, '\\$&')
|
1022
|
+
.replace(optionalParam, '(?:$1)?')
|
930
1023
|
.replace(namedParam, '([^\/]+)')
|
931
1024
|
.replace(splatParam, '(.*?)');
|
932
1025
|
return new RegExp('^' + route + '$');
|
@@ -948,14 +1041,26 @@
|
|
948
1041
|
var History = Backbone.History = function() {
|
949
1042
|
this.handlers = [];
|
950
1043
|
_.bindAll(this, 'checkUrl');
|
1044
|
+
|
1045
|
+
// #1653 - Ensure that `History` can be used outside of the browser.
|
1046
|
+
if (typeof window !== 'undefined') {
|
1047
|
+
this.location = window.location;
|
1048
|
+
this.history = window.history;
|
1049
|
+
}
|
951
1050
|
};
|
952
1051
|
|
953
|
-
// Cached regex for
|
954
|
-
var routeStripper = /^[#\/]
|
1052
|
+
// Cached regex for stripping a leading hash/slash and trailing space.
|
1053
|
+
var routeStripper = /^[#\/]|\s+$/g;
|
1054
|
+
|
1055
|
+
// Cached regex for stripping leading and trailing slashes.
|
1056
|
+
var rootStripper = /^\/+|\/+$/g;
|
955
1057
|
|
956
1058
|
// Cached regex for detecting MSIE.
|
957
1059
|
var isExplorer = /msie [\w.]+/;
|
958
1060
|
|
1061
|
+
// Cached regex for removing a trailing slash.
|
1062
|
+
var trailingSlash = /\/$/;
|
1063
|
+
|
959
1064
|
// Has the history handling already been started?
|
960
1065
|
History.started = false;
|
961
1066
|
|
@@ -968,9 +1073,8 @@
|
|
968
1073
|
|
969
1074
|
// Gets the true hash value. Cannot use location.hash directly due to bug
|
970
1075
|
// in Firefox where location.hash will always be decoded.
|
971
|
-
getHash: function(
|
972
|
-
var
|
973
|
-
var match = loc.href.match(/#(.*)$/);
|
1076
|
+
getHash: function(window) {
|
1077
|
+
var match = (window || this).location.href.match(/#(.*)$/);
|
974
1078
|
return match ? match[1] : '';
|
975
1079
|
},
|
976
1080
|
|
@@ -978,15 +1082,14 @@
|
|
978
1082
|
// the hash, or the override.
|
979
1083
|
getFragment: function(fragment, forcePushState) {
|
980
1084
|
if (fragment == null) {
|
981
|
-
if (this._hasPushState || forcePushState) {
|
982
|
-
fragment =
|
983
|
-
var
|
984
|
-
if (
|
1085
|
+
if (this._hasPushState || !this._wantsHashChange || forcePushState) {
|
1086
|
+
fragment = this.location.pathname;
|
1087
|
+
var root = this.root.replace(trailingSlash, '');
|
1088
|
+
if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
|
985
1089
|
} else {
|
986
1090
|
fragment = this.getHash();
|
987
1091
|
}
|
988
1092
|
}
|
989
|
-
if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
|
990
1093
|
return fragment.replace(routeStripper, '');
|
991
1094
|
},
|
992
1095
|
|
@@ -999,24 +1102,28 @@
|
|
999
1102
|
// Figure out the initial configuration. Do we need an iframe?
|
1000
1103
|
// Is pushState desired ... is it available?
|
1001
1104
|
this.options = _.extend({}, {root: '/'}, this.options, options);
|
1105
|
+
this.root = this.options.root;
|
1002
1106
|
this._wantsHashChange = this.options.hashChange !== false;
|
1003
1107
|
this._wantsPushState = !!this.options.pushState;
|
1004
|
-
this._hasPushState = !!(this.options.pushState &&
|
1108
|
+
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
|
1005
1109
|
var fragment = this.getFragment();
|
1006
1110
|
var docMode = document.documentMode;
|
1007
1111
|
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
|
1008
1112
|
|
1009
|
-
|
1010
|
-
|
1113
|
+
// Normalize root to always include a leading and trailing slash.
|
1114
|
+
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
|
1115
|
+
|
1116
|
+
if (oldIE && this._wantsHashChange) {
|
1117
|
+
this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
|
1011
1118
|
this.navigate(fragment);
|
1012
1119
|
}
|
1013
1120
|
|
1014
1121
|
// Depending on whether we're using pushState or hashes, and whether
|
1015
1122
|
// 'onhashchange' is supported, determine how we check the URL state.
|
1016
1123
|
if (this._hasPushState) {
|
1017
|
-
|
1124
|
+
Backbone.$(window).bind('popstate', this.checkUrl);
|
1018
1125
|
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
|
1019
|
-
|
1126
|
+
Backbone.$(window).bind('hashchange', this.checkUrl);
|
1020
1127
|
} else if (this._wantsHashChange) {
|
1021
1128
|
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
|
1022
1129
|
}
|
@@ -1024,14 +1131,14 @@
|
|
1024
1131
|
// Determine if we need to change the base url, for a pushState link
|
1025
1132
|
// opened by a non-pushState browser.
|
1026
1133
|
this.fragment = fragment;
|
1027
|
-
var loc =
|
1028
|
-
var atRoot
|
1134
|
+
var loc = this.location;
|
1135
|
+
var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
|
1029
1136
|
|
1030
1137
|
// If we've started off with a route from a `pushState`-enabled browser,
|
1031
1138
|
// but we're currently in a browser that doesn't support it...
|
1032
1139
|
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
|
1033
1140
|
this.fragment = this.getFragment(null, true);
|
1034
|
-
|
1141
|
+
this.location.replace(this.root + this.location.search + '#' + this.fragment);
|
1035
1142
|
// Return immediately as browser will do redirect to new url
|
1036
1143
|
return true;
|
1037
1144
|
|
@@ -1039,18 +1146,16 @@
|
|
1039
1146
|
// in a browser where it could be `pushState`-based instead...
|
1040
1147
|
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
|
1041
1148
|
this.fragment = this.getHash().replace(routeStripper, '');
|
1042
|
-
|
1149
|
+
this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
|
1043
1150
|
}
|
1044
1151
|
|
1045
|
-
if (!this.options.silent)
|
1046
|
-
return this.loadUrl();
|
1047
|
-
}
|
1152
|
+
if (!this.options.silent) return this.loadUrl();
|
1048
1153
|
},
|
1049
1154
|
|
1050
1155
|
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
|
1051
1156
|
// but possibly useful for unit testing Routers.
|
1052
1157
|
stop: function() {
|
1053
|
-
|
1158
|
+
Backbone.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
|
1054
1159
|
clearInterval(this._checkUrlInterval);
|
1055
1160
|
History.started = false;
|
1056
1161
|
},
|
@@ -1065,8 +1170,10 @@
|
|
1065
1170
|
// calls `loadUrl`, normalizing across the hidden iframe.
|
1066
1171
|
checkUrl: function(e) {
|
1067
1172
|
var current = this.getFragment();
|
1068
|
-
if (current
|
1069
|
-
|
1173
|
+
if (current === this.fragment && this.iframe) {
|
1174
|
+
current = this.getFragment(this.getHash(this.iframe));
|
1175
|
+
}
|
1176
|
+
if (current === this.fragment) return false;
|
1070
1177
|
if (this.iframe) this.navigate(current);
|
1071
1178
|
this.loadUrl() || this.loadUrl(this.getHash());
|
1072
1179
|
},
|
@@ -1095,31 +1202,31 @@
|
|
1095
1202
|
navigate: function(fragment, options) {
|
1096
1203
|
if (!History.started) return false;
|
1097
1204
|
if (!options || options === true) options = {trigger: options};
|
1098
|
-
|
1099
|
-
if (this.fragment
|
1205
|
+
fragment = this.getFragment(fragment || '');
|
1206
|
+
if (this.fragment === fragment) return;
|
1207
|
+
this.fragment = fragment;
|
1208
|
+
var url = this.root + fragment;
|
1100
1209
|
|
1101
1210
|
// If pushState is available, we use it to set the fragment as a real URL.
|
1102
1211
|
if (this._hasPushState) {
|
1103
|
-
|
1104
|
-
this.fragment = frag;
|
1105
|
-
window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
|
1212
|
+
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
|
1106
1213
|
|
1107
1214
|
// If hash changes haven't been explicitly disabled, update the hash
|
1108
1215
|
// fragment to store history.
|
1109
1216
|
} else if (this._wantsHashChange) {
|
1110
|
-
this.fragment
|
1111
|
-
this.
|
1112
|
-
|
1113
|
-
//
|
1114
|
-
//
|
1217
|
+
this._updateHash(this.location, fragment, options.replace);
|
1218
|
+
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
|
1219
|
+
// Opening and closing the iframe tricks IE7 and earlier to push a
|
1220
|
+
// history entry on hash-tag change. When replace is true, we don't
|
1221
|
+
// want this.
|
1115
1222
|
if(!options.replace) this.iframe.document.open().close();
|
1116
|
-
this._updateHash(this.iframe.location,
|
1223
|
+
this._updateHash(this.iframe.location, fragment, options.replace);
|
1117
1224
|
}
|
1118
1225
|
|
1119
1226
|
// If you've told us that you explicitly don't want fallback hashchange-
|
1120
1227
|
// based history, then `navigate` becomes a page refresh.
|
1121
1228
|
} else {
|
1122
|
-
|
1229
|
+
return this.location.assign(url);
|
1123
1230
|
}
|
1124
1231
|
if (options.trigger) this.loadUrl(fragment);
|
1125
1232
|
},
|
@@ -1128,13 +1235,19 @@
|
|
1128
1235
|
// a new one to the browser history.
|
1129
1236
|
_updateHash: function(location, fragment, replace) {
|
1130
1237
|
if (replace) {
|
1131
|
-
location.
|
1238
|
+
var href = location.href.replace(/(javascript:|#).*$/, '');
|
1239
|
+
location.replace(href + '#' + fragment);
|
1132
1240
|
} else {
|
1133
|
-
|
1241
|
+
// #1649 - Some browsers require that `hash` contains a leading #.
|
1242
|
+
location.hash = '#' + fragment;
|
1134
1243
|
}
|
1135
1244
|
}
|
1245
|
+
|
1136
1246
|
});
|
1137
1247
|
|
1248
|
+
// Create the default Backbone.history.
|
1249
|
+
Backbone.history = new History;
|
1250
|
+
|
1138
1251
|
// Backbone.View
|
1139
1252
|
// -------------
|
1140
1253
|
|
@@ -1152,7 +1265,7 @@
|
|
1152
1265
|
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
1153
1266
|
|
1154
1267
|
// List of view options to be merged as properties.
|
1155
|
-
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
|
1268
|
+
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
|
1156
1269
|
|
1157
1270
|
// Set up all inheritable **Backbone.View** properties and methods.
|
1158
1271
|
_.extend(View.prototype, Events, {
|
@@ -1177,10 +1290,11 @@
|
|
1177
1290
|
return this;
|
1178
1291
|
},
|
1179
1292
|
|
1180
|
-
// Remove this view
|
1181
|
-
//
|
1293
|
+
// Remove this view by taking the element out of the DOM, and removing any
|
1294
|
+
// applicable Backbone.Events listeners.
|
1182
1295
|
remove: function() {
|
1183
1296
|
this.$el.remove();
|
1297
|
+
this.stopListening();
|
1184
1298
|
return this;
|
1185
1299
|
},
|
1186
1300
|
|
@@ -1191,8 +1305,8 @@
|
|
1191
1305
|
//
|
1192
1306
|
make: function(tagName, attributes, content) {
|
1193
1307
|
var el = document.createElement(tagName);
|
1194
|
-
if (attributes)
|
1195
|
-
if (content)
|
1308
|
+
if (attributes) Backbone.$(el).attr(attributes);
|
1309
|
+
if (content != null) Backbone.$(el).html(content);
|
1196
1310
|
return el;
|
1197
1311
|
},
|
1198
1312
|
|
@@ -1200,7 +1314,7 @@
|
|
1200
1314
|
// re-delegation.
|
1201
1315
|
setElement: function(element, delegate) {
|
1202
1316
|
if (this.$el) this.undelegateEvents();
|
1203
|
-
this.$el =
|
1317
|
+
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
|
1204
1318
|
this.el = this.$el[0];
|
1205
1319
|
if (delegate !== false) this.delegateEvents();
|
1206
1320
|
return this;
|
@@ -1222,7 +1336,7 @@
|
|
1222
1336
|
// This only works for delegate-able events: not `focus`, `blur`, and
|
1223
1337
|
// not `change`, `submit`, and `reset` in Internet Explorer.
|
1224
1338
|
delegateEvents: function(events) {
|
1225
|
-
if (!(events || (events =
|
1339
|
+
if (!(events || (events = _.result(this, 'events')))) return;
|
1226
1340
|
this.undelegateEvents();
|
1227
1341
|
for (var key in events) {
|
1228
1342
|
var method = events[key];
|
@@ -1251,11 +1365,8 @@
|
|
1251
1365
|
// Keys with special meaning *(model, collection, id, className)*, are
|
1252
1366
|
// attached directly to the view.
|
1253
1367
|
_configure: function(options) {
|
1254
|
-
if (this.options) options = _.extend({}, this
|
1255
|
-
|
1256
|
-
var attr = viewOptions[i];
|
1257
|
-
if (options[attr]) this[attr] = options[attr];
|
1258
|
-
}
|
1368
|
+
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
|
1369
|
+
_.extend(this, _.pick(options, viewOptions));
|
1259
1370
|
this.options = options;
|
1260
1371
|
},
|
1261
1372
|
|
@@ -1265,27 +1376,17 @@
|
|
1265
1376
|
// an element from the `id`, `className` and `tagName` properties.
|
1266
1377
|
_ensureElement: function() {
|
1267
1378
|
if (!this.el) {
|
1268
|
-
var attrs =
|
1269
|
-
if (this.id) attrs.id = this
|
1270
|
-
if (this.className) attrs['class'] = this
|
1271
|
-
this.setElement(this.make(this
|
1379
|
+
var attrs = _.extend({}, _.result(this, 'attributes'));
|
1380
|
+
if (this.id) attrs.id = _.result(this, 'id');
|
1381
|
+
if (this.className) attrs['class'] = _.result(this, 'className');
|
1382
|
+
this.setElement(this.make(_.result(this, 'tagName'), attrs), false);
|
1272
1383
|
} else {
|
1273
|
-
this.setElement(this
|
1384
|
+
this.setElement(_.result(this, 'el'), false);
|
1274
1385
|
}
|
1275
1386
|
}
|
1276
1387
|
|
1277
1388
|
});
|
1278
1389
|
|
1279
|
-
// The self-propagating extend function that Backbone classes use.
|
1280
|
-
var extend = function (protoProps, classProps) {
|
1281
|
-
var child = inherits(this, protoProps, classProps);
|
1282
|
-
child.extend = this.extend;
|
1283
|
-
return child;
|
1284
|
-
};
|
1285
|
-
|
1286
|
-
// Set up inheritance for the model, collection, and view.
|
1287
|
-
Model.extend = Collection.extend = Router.extend = View.extend = extend;
|
1288
|
-
|
1289
1390
|
// Backbone.sync
|
1290
1391
|
// -------------
|
1291
1392
|
|
@@ -1293,6 +1394,7 @@
|
|
1293
1394
|
var methodMap = {
|
1294
1395
|
'create': 'POST',
|
1295
1396
|
'update': 'PUT',
|
1397
|
+
'patch': 'PATCH',
|
1296
1398
|
'delete': 'DELETE',
|
1297
1399
|
'read': 'GET'
|
1298
1400
|
};
|
@@ -1316,112 +1418,112 @@
|
|
1316
1418
|
var type = methodMap[method];
|
1317
1419
|
|
1318
1420
|
// Default options, unless specified.
|
1319
|
-
options || (options = {})
|
1421
|
+
_.defaults(options || (options = {}), {
|
1422
|
+
emulateHTTP: Backbone.emulateHTTP,
|
1423
|
+
emulateJSON: Backbone.emulateJSON
|
1424
|
+
});
|
1320
1425
|
|
1321
1426
|
// Default JSON-request options.
|
1322
1427
|
var params = {type: type, dataType: 'json'};
|
1323
1428
|
|
1324
1429
|
// Ensure that we have a URL.
|
1325
1430
|
if (!options.url) {
|
1326
|
-
params.url =
|
1431
|
+
params.url = _.result(model, 'url') || urlError();
|
1327
1432
|
}
|
1328
1433
|
|
1329
1434
|
// Ensure that we have the appropriate request data.
|
1330
|
-
if (
|
1435
|
+
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
1331
1436
|
params.contentType = 'application/json';
|
1332
|
-
params.data = JSON.stringify(model.toJSON());
|
1437
|
+
params.data = JSON.stringify(options.attrs || model.toJSON(options));
|
1333
1438
|
}
|
1334
1439
|
|
1335
1440
|
// For older servers, emulate JSON by encoding the request into an HTML-form.
|
1336
|
-
if (
|
1441
|
+
if (options.emulateJSON) {
|
1337
1442
|
params.contentType = 'application/x-www-form-urlencoded';
|
1338
1443
|
params.data = params.data ? {model: params.data} : {};
|
1339
1444
|
}
|
1340
1445
|
|
1341
1446
|
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
|
1342
1447
|
// And an `X-HTTP-Method-Override` header.
|
1343
|
-
if (
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
}
|
1448
|
+
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
|
1449
|
+
params.type = 'POST';
|
1450
|
+
if (options.emulateJSON) params.data._method = type;
|
1451
|
+
var beforeSend = options.beforeSend;
|
1452
|
+
options.beforeSend = function(xhr) {
|
1453
|
+
xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
1454
|
+
if (beforeSend) return beforeSend.apply(this, arguments);
|
1455
|
+
};
|
1351
1456
|
}
|
1352
1457
|
|
1353
1458
|
// Don't process data on a non-GET request.
|
1354
|
-
if (params.type !== 'GET' && !
|
1459
|
+
if (params.type !== 'GET' && !options.emulateJSON) {
|
1355
1460
|
params.processData = false;
|
1356
1461
|
}
|
1357
1462
|
|
1463
|
+
var success = options.success;
|
1464
|
+
options.success = function(resp, status, xhr) {
|
1465
|
+
if (success) success(resp, status, xhr);
|
1466
|
+
model.trigger('sync', model, resp, options);
|
1467
|
+
};
|
1468
|
+
|
1469
|
+
var error = options.error;
|
1470
|
+
options.error = function(xhr, status, thrown) {
|
1471
|
+
if (error) error(model, xhr, options);
|
1472
|
+
model.trigger('error', model, xhr, options);
|
1473
|
+
};
|
1474
|
+
|
1358
1475
|
// Make the request, allowing the user to override any Ajax options.
|
1359
|
-
|
1476
|
+
var xhr = Backbone.ajax(_.extend(params, options));
|
1477
|
+
model.trigger('request', model, xhr, options);
|
1478
|
+
return xhr;
|
1360
1479
|
};
|
1361
1480
|
|
1362
|
-
//
|
1363
|
-
Backbone.
|
1364
|
-
return
|
1365
|
-
resp = model === originalModel ? resp : model;
|
1366
|
-
if (onError) {
|
1367
|
-
onError(originalModel, resp, options);
|
1368
|
-
} else {
|
1369
|
-
originalModel.trigger('error', originalModel, resp, options);
|
1370
|
-
}
|
1371
|
-
};
|
1481
|
+
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
|
1482
|
+
Backbone.ajax = function() {
|
1483
|
+
return Backbone.$.ajax.apply(Backbone.$, arguments);
|
1372
1484
|
};
|
1373
1485
|
|
1374
1486
|
// Helpers
|
1375
1487
|
// -------
|
1376
1488
|
|
1377
|
-
// Shared empty constructor function to aid in prototype-chain creation.
|
1378
|
-
var ctor = function(){};
|
1379
|
-
|
1380
1489
|
// Helper function to correctly set up the prototype chain, for subclasses.
|
1381
1490
|
// Similar to `goog.inherits`, but uses a hash of prototype properties and
|
1382
1491
|
// class properties to be extended.
|
1383
|
-
var
|
1492
|
+
var extend = function(protoProps, staticProps) {
|
1493
|
+
var parent = this;
|
1384
1494
|
var child;
|
1385
1495
|
|
1386
1496
|
// The constructor function for the new subclass is either defined by you
|
1387
1497
|
// (the "constructor" property in your `extend` definition), or defaulted
|
1388
1498
|
// by us to simply call the parent's constructor.
|
1389
|
-
if (protoProps &&
|
1499
|
+
if (protoProps && _.has(protoProps, 'constructor')) {
|
1390
1500
|
child = protoProps.constructor;
|
1391
1501
|
} else {
|
1392
1502
|
child = function(){ parent.apply(this, arguments); };
|
1393
1503
|
}
|
1394
1504
|
|
1395
|
-
//
|
1396
|
-
_.extend(child, parent);
|
1505
|
+
// Add static properties to the constructor function, if supplied.
|
1506
|
+
_.extend(child, parent, staticProps);
|
1397
1507
|
|
1398
1508
|
// Set the prototype chain to inherit from `parent`, without calling
|
1399
1509
|
// `parent`'s constructor function.
|
1400
|
-
|
1401
|
-
|
1510
|
+
var Surrogate = function(){ this.constructor = child; };
|
1511
|
+
Surrogate.prototype = parent.prototype;
|
1512
|
+
child.prototype = new Surrogate;
|
1402
1513
|
|
1403
1514
|
// Add prototype properties (instance properties) to the subclass,
|
1404
1515
|
// if supplied.
|
1405
1516
|
if (protoProps) _.extend(child.prototype, protoProps);
|
1406
1517
|
|
1407
|
-
//
|
1408
|
-
|
1409
|
-
|
1410
|
-
// Correctly set child's `prototype.constructor`.
|
1411
|
-
child.prototype.constructor = child;
|
1412
|
-
|
1413
|
-
// Set a convenience property in case the parent's prototype is needed later.
|
1518
|
+
// Set a convenience property in case the parent's prototype is needed
|
1519
|
+
// later.
|
1414
1520
|
child.__super__ = parent.prototype;
|
1415
1521
|
|
1416
1522
|
return child;
|
1417
1523
|
};
|
1418
1524
|
|
1419
|
-
//
|
1420
|
-
|
1421
|
-
var getValue = function(object, prop) {
|
1422
|
-
if (!(object && object[prop])) return null;
|
1423
|
-
return _.isFunction(object[prop]) ? object[prop]() : object[prop];
|
1424
|
-
};
|
1525
|
+
// Set up inheritance for the model, collection, router, view and history.
|
1526
|
+
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
|
1425
1527
|
|
1426
1528
|
// Throw an error when a URL is needed, and none is supplied.
|
1427
1529
|
var urlError = function() {
|