ultimate-base 0.5.0.0 → 0.6.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/app/assets/javascripts/ultimate/backbone/app.js.coffee +47 -5
- data/app/assets/javascripts/ultimate/backbone/base.js.coffee +9 -10
- data/app/assets/javascripts/ultimate/backbone/collection.js.coffee +0 -1
- data/app/assets/javascripts/ultimate/backbone/lib/backbone.js +765 -673
- data/app/assets/javascripts/ultimate/backbone/model.js.coffee +0 -1
- data/app/assets/javascripts/ultimate/backbone/router.js.coffee +0 -4
- data/app/assets/javascripts/ultimate/backbone/view-mixins/nodes.js.coffee +51 -0
- data/app/assets/javascripts/ultimate/backbone/view.js.coffee +18 -104
- data/app/assets/javascripts/ultimate/base.js.coffee +1 -11
- data/app/assets/javascripts/ultimate/jquery-plugin-adapter.js.coffee +0 -1
- data/app/assets/javascripts/ultimate/jquery-plugin-class.js.coffee +3 -25
- data/app/assets/javascripts/ultimate/jquery.base.js.coffee +0 -2
- data/app/assets/javascripts/ultimate/underscore/underscore.inflection.js +4 -3
- data/app/assets/javascripts/ultimate/underscore/underscore.js +103 -80
- data/app/assets/javascripts/ultimate/underscore/underscore.string.js +71 -27
- data/lib/ultimate/base.rb +0 -1
- data/lib/ultimate/base/version.rb +1 -1
- metadata +3 -8
- data/app/assets/javascripts/ultimate/backbone/extra/jquery-ext.js.coffee +0 -96
- data/app/assets/javascripts/ultimate/improves/datepicker.js.coffee +0 -34
- data/app/assets/javascripts/ultimate/improves/devise.js.coffee +0 -18
- data/app/assets/javascripts/ultimate/improves/form-errors.js.coffee +0 -146
- data/app/assets/javascripts/ultimate/improves/tablesorter.js +0 -59
- data/lib/ultimate/extensions/directive_processor.rb +0 -64
@@ -8,18 +8,60 @@ class Ultimate.Backbone.App
|
|
8
8
|
Models: {}
|
9
9
|
Collections: {}
|
10
10
|
Routers: {}
|
11
|
+
ViewMixins: {}
|
12
|
+
ProtoViews: {}
|
11
13
|
Views: {}
|
12
|
-
|
13
|
-
scopes: ["Models", "Collections", "Routers", "Views"]
|
14
|
+
viewInstances: []
|
14
15
|
|
15
16
|
constructor: (name = null) ->
|
16
|
-
Ultimate.Backbone.debug ".App.constructor()", @
|
17
17
|
if @constructor.App
|
18
|
-
throw new Error(
|
18
|
+
throw new Error('Can\'t create new Ultimate.Backbone.App because the single instance has already been created')
|
19
19
|
else
|
20
|
+
cout 'info', 'Ultimate.Backbone.App.constructor', name, @
|
20
21
|
@constructor.App = @
|
21
22
|
@name = name
|
22
|
-
|
23
|
+
|
24
|
+
start: ->
|
25
|
+
@bindViews()
|
26
|
+
@bindCustomElements()
|
27
|
+
|
28
|
+
bindViews: (jRoot = $('html')) ->
|
29
|
+
bindedViews = []
|
30
|
+
for viewName, viewClass of @Views when viewClass::el
|
31
|
+
#cout 'info', "Try bind #{viewName} [#{viewClass::el}]"
|
32
|
+
jRoot.find(viewClass::el).each (index, el) =>
|
33
|
+
view = new viewClass(el: el)
|
34
|
+
cout 'info', "Binded view #{viewName}:", view
|
35
|
+
@viewInstances.push view
|
36
|
+
bindedViews.push view
|
37
|
+
bindedViews
|
38
|
+
|
39
|
+
unbindViews: (views) ->
|
40
|
+
view.undelegateEvents() for view in views
|
41
|
+
@viewInstances = _.without(@viewInstances, views...)
|
42
|
+
|
43
|
+
getFirstView: (viewClass) ->
|
44
|
+
for view in @viewInstances
|
45
|
+
if view.constructor is viewClass
|
46
|
+
return view
|
47
|
+
return null
|
48
|
+
|
49
|
+
getAllViews: (viewClass) ->
|
50
|
+
_.filter(@viewInstances, (view) -> view.constructor is viewClass)
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
customElementBinders: []
|
55
|
+
|
56
|
+
registerCustomElementBinder: (binder) ->
|
57
|
+
@customElementBinders.push binder
|
58
|
+
|
59
|
+
bindCustomElements: (jRoot = $('body')) ->
|
60
|
+
for binder in @customElementBinders
|
61
|
+
if _.isFunction(binder)
|
62
|
+
binder jRoot
|
63
|
+
else
|
64
|
+
jRoot.find(binder['selector'])[binder['method']] binder['arguments']...
|
23
65
|
|
24
66
|
|
25
67
|
|
@@ -2,18 +2,11 @@
|
|
2
2
|
# jquery ~> 1.7.0
|
3
3
|
# underscore ~> 1.3.0
|
4
4
|
|
5
|
-
#= require
|
6
|
-
#= require ultimate/helpers
|
5
|
+
#= require ../base
|
7
6
|
|
8
|
-
|
7
|
+
Ultimate.Backbone ||=
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
debug: ->
|
13
|
-
if @debugMode
|
14
|
-
a = ["info", "Ultimate.Backbone"]
|
15
|
-
Array::push.apply a, arguments if arguments.length > 0
|
16
|
-
cout.apply @, a
|
9
|
+
ViewMixins: {}
|
17
10
|
|
18
11
|
isView: (view) -> view instanceof Backbone.View
|
19
12
|
|
@@ -24,3 +17,9 @@
|
|
24
17
|
isCollection: (collection) -> collection instanceof Backbone.Collection
|
25
18
|
|
26
19
|
isRouter: (router) -> router instanceof Backbone.Router
|
20
|
+
|
21
|
+
# MixinSupport:
|
22
|
+
# include: (mixin) ->
|
23
|
+
# unless mixin?
|
24
|
+
# throw new Error('Mixin is undefined')
|
25
|
+
# _.extend @::, mixin
|
@@ -1,6 +1,6 @@
|
|
1
|
-
// Backbone.js 0.
|
1
|
+
// Backbone.js 1.0.0
|
2
2
|
|
3
|
-
// (c) 2010-
|
3
|
+
// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc.
|
4
4
|
// Backbone may be freely distributed under the MIT license.
|
5
5
|
// For all details and documentation:
|
6
6
|
// http://backbonejs.org
|
@@ -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,14 +18,14 @@
|
|
18
18
|
// restored later on, if `noConflict` is used.
|
19
19
|
var previousBackbone = root.Backbone;
|
20
20
|
|
21
|
-
// Create
|
22
|
-
var
|
23
|
-
var push =
|
24
|
-
var slice =
|
25
|
-
var splice =
|
21
|
+
// Create local references to array methods we'll want to use later.
|
22
|
+
var array = [];
|
23
|
+
var push = array.push;
|
24
|
+
var slice = array.slice;
|
25
|
+
var splice = array.splice;
|
26
26
|
|
27
27
|
// The top-level namespace. All public Backbone classes and modules will
|
28
|
-
// be attached to this. Exported for both
|
28
|
+
// be attached to this. Exported for both the browser and the server.
|
29
29
|
var Backbone;
|
30
30
|
if (typeof exports !== 'undefined') {
|
31
31
|
Backbone = exports;
|
@@ -34,14 +34,15 @@
|
|
34
34
|
}
|
35
35
|
|
36
36
|
// Current version of the library. Keep in sync with `package.json`.
|
37
|
-
Backbone.VERSION = '0.
|
37
|
+
Backbone.VERSION = '1.0.0';
|
38
38
|
|
39
39
|
// Require Underscore, if we're on the server, and it's not already present.
|
40
40
|
var _ = root._;
|
41
41
|
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
|
42
42
|
|
43
|
-
// For Backbone's purposes, jQuery, Zepto, or
|
44
|
-
|
43
|
+
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
|
44
|
+
// the `$` variable.
|
45
|
+
Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
|
45
46
|
|
46
47
|
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
|
47
48
|
// to its previous owner. Returns a reference to this Backbone object.
|
@@ -62,14 +63,12 @@
|
|
62
63
|
Backbone.emulateJSON = false;
|
63
64
|
|
64
65
|
// Backbone.Events
|
65
|
-
//
|
66
|
-
|
67
|
-
// Regular expression used to split event strings
|
68
|
-
var eventSplitter = /\s+/;
|
66
|
+
// ---------------
|
69
67
|
|
70
68
|
// A module that can be mixed in to *any object* in order to provide it with
|
71
|
-
// custom events. You may bind with `on` or remove with `off` callback
|
72
|
-
// to an event; `trigger`-ing an event fires all callbacks in
|
69
|
+
// custom events. You may bind with `on` or remove with `off` callback
|
70
|
+
// functions to an event; `trigger`-ing an event fires all callbacks in
|
71
|
+
// succession.
|
73
72
|
//
|
74
73
|
// var object = {};
|
75
74
|
// _.extend(object, Backbone.Events);
|
@@ -78,49 +77,56 @@
|
|
78
77
|
//
|
79
78
|
var Events = Backbone.Events = {
|
80
79
|
|
81
|
-
// Bind
|
82
|
-
//
|
83
|
-
on: function(
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
events
|
88
|
-
calls = this._callbacks || (this._callbacks = {});
|
89
|
-
|
90
|
-
while (event = events.shift()) {
|
91
|
-
list = calls[event] || (calls[event] = []);
|
92
|
-
list.push(callback, context);
|
93
|
-
}
|
94
|
-
|
80
|
+
// Bind an event to a `callback` function. Passing `"all"` will bind
|
81
|
+
// the callback to all events fired.
|
82
|
+
on: function(name, callback, context) {
|
83
|
+
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
|
84
|
+
this._events || (this._events = {});
|
85
|
+
var events = this._events[name] || (this._events[name] = []);
|
86
|
+
events.push({callback: callback, context: context, ctx: context || this});
|
95
87
|
return this;
|
96
88
|
},
|
97
89
|
|
98
|
-
//
|
99
|
-
//
|
100
|
-
|
101
|
-
|
102
|
-
var
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
90
|
+
// Bind an event to only be triggered a single time. After the first time
|
91
|
+
// the callback is invoked, it will be removed.
|
92
|
+
once: function(name, callback, context) {
|
93
|
+
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
|
94
|
+
var self = this;
|
95
|
+
var once = _.once(function() {
|
96
|
+
self.off(name, once);
|
97
|
+
callback.apply(this, arguments);
|
98
|
+
});
|
99
|
+
once._callback = callback;
|
100
|
+
return this.on(name, once, context);
|
101
|
+
},
|
102
|
+
|
103
|
+
// Remove one or many callbacks. If `context` is null, removes all
|
104
|
+
// callbacks with that function. If `callback` is null, removes all
|
105
|
+
// callbacks for the event. If `name` is null, removes all bound
|
106
|
+
// callbacks for all events.
|
107
|
+
off: function(name, callback, context) {
|
108
|
+
var retain, ev, events, names, i, l, j, k;
|
109
|
+
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
|
110
|
+
if (!name && !callback && !context) {
|
111
|
+
this._events = {};
|
108
112
|
return this;
|
109
113
|
}
|
110
114
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
115
|
+
names = name ? [name] : _.keys(this._events);
|
116
|
+
for (i = 0, l = names.length; i < l; i++) {
|
117
|
+
name = names[i];
|
118
|
+
if (events = this._events[name]) {
|
119
|
+
this._events[name] = retain = [];
|
120
|
+
if (callback || context) {
|
121
|
+
for (j = 0, k = events.length; j < k; j++) {
|
122
|
+
ev = events[j];
|
123
|
+
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
|
124
|
+
(context && context !== ev.context)) {
|
125
|
+
retain.push(ev);
|
126
|
+
}
|
127
|
+
}
|
123
128
|
}
|
129
|
+
if (!retain.length) delete this._events[name];
|
124
130
|
}
|
125
131
|
}
|
126
132
|
|
@@ -131,96 +137,138 @@
|
|
131
137
|
// passed the same arguments as `trigger` is, apart from the event name
|
132
138
|
// (unless you're listening on `"all"`, which will cause your callback to
|
133
139
|
// receive the true name of the event as the first argument).
|
134
|
-
trigger: function(
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
+
trigger: function(name) {
|
141
|
+
if (!this._events) return this;
|
142
|
+
var args = slice.call(arguments, 1);
|
143
|
+
if (!eventsApi(this, 'trigger', name, args)) return this;
|
144
|
+
var events = this._events[name];
|
145
|
+
var allEvents = this._events.all;
|
146
|
+
if (events) triggerEvents(events, args);
|
147
|
+
if (allEvents) triggerEvents(allEvents, arguments);
|
148
|
+
return this;
|
149
|
+
},
|
140
150
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
151
|
+
// Tell this object to stop listening to either specific events ... or
|
152
|
+
// to every object it's currently listening to.
|
153
|
+
stopListening: function(obj, name, callback) {
|
154
|
+
var listeners = this._listeners;
|
155
|
+
if (!listeners) return this;
|
156
|
+
var deleteListener = !name && !callback;
|
157
|
+
if (typeof name === 'object') callback = this;
|
158
|
+
if (obj) (listeners = {})[obj._listenerId] = obj;
|
159
|
+
for (var id in listeners) {
|
160
|
+
listeners[id].off(name, callback, this);
|
161
|
+
if (deleteListener) delete this._listeners[id];
|
145
162
|
}
|
163
|
+
return this;
|
164
|
+
}
|
146
165
|
|
147
|
-
|
148
|
-
// trigger the event, then to trigger any `"all"` callbacks.
|
149
|
-
while (event = events.shift()) {
|
150
|
-
// Copy callback lists to prevent modification.
|
151
|
-
if (all = calls.all) all = all.slice();
|
152
|
-
if (list = calls[event]) list = list.slice();
|
153
|
-
|
154
|
-
// Execute event callbacks.
|
155
|
-
if (list) {
|
156
|
-
for (i = 0, length = list.length; i < length; i += 2) {
|
157
|
-
list[i].apply(list[i + 1] || this, rest);
|
158
|
-
}
|
159
|
-
}
|
166
|
+
};
|
160
167
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
+
// Regular expression used to split event strings.
|
169
|
+
var eventSplitter = /\s+/;
|
170
|
+
|
171
|
+
// Implement fancy features of the Events API such as multiple event
|
172
|
+
// names `"change blur"` and jQuery-style event maps `{change: action}`
|
173
|
+
// in terms of the existing API.
|
174
|
+
var eventsApi = function(obj, action, name, rest) {
|
175
|
+
if (!name) return true;
|
176
|
+
|
177
|
+
// Handle event maps.
|
178
|
+
if (typeof name === 'object') {
|
179
|
+
for (var key in name) {
|
180
|
+
obj[action].apply(obj, [key, name[key]].concat(rest));
|
168
181
|
}
|
182
|
+
return false;
|
183
|
+
}
|
169
184
|
|
170
|
-
|
185
|
+
// Handle space separated event names.
|
186
|
+
if (eventSplitter.test(name)) {
|
187
|
+
var names = name.split(eventSplitter);
|
188
|
+
for (var i = 0, l = names.length; i < l; i++) {
|
189
|
+
obj[action].apply(obj, [names[i]].concat(rest));
|
190
|
+
}
|
191
|
+
return false;
|
171
192
|
}
|
172
193
|
|
194
|
+
return true;
|
195
|
+
};
|
196
|
+
|
197
|
+
// A difficult-to-believe, but optimized internal dispatch function for
|
198
|
+
// triggering events. Tries to keep the usual cases speedy (most internal
|
199
|
+
// Backbone events have 3 arguments).
|
200
|
+
var triggerEvents = function(events, args) {
|
201
|
+
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
|
202
|
+
switch (args.length) {
|
203
|
+
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
|
204
|
+
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
|
205
|
+
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
|
206
|
+
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);
|
208
|
+
}
|
173
209
|
};
|
174
210
|
|
211
|
+
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
|
212
|
+
|
213
|
+
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
|
214
|
+
// listen to an event in another object ... keeping track of what it's
|
215
|
+
// listening to.
|
216
|
+
_.each(listenMethods, function(implementation, method) {
|
217
|
+
Events[method] = function(obj, name, callback) {
|
218
|
+
var listeners = this._listeners || (this._listeners = {});
|
219
|
+
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
|
220
|
+
listeners[id] = obj;
|
221
|
+
if (typeof name === 'object') callback = this;
|
222
|
+
obj[implementation](name, callback, this);
|
223
|
+
return this;
|
224
|
+
};
|
225
|
+
});
|
226
|
+
|
175
227
|
// Aliases for backwards compatibility.
|
176
228
|
Events.bind = Events.on;
|
177
229
|
Events.unbind = Events.off;
|
178
230
|
|
231
|
+
// Allow the `Backbone` object to serve as a global event bus, for folks who
|
232
|
+
// want global "pubsub" in a convenient place.
|
233
|
+
_.extend(Backbone, Events);
|
234
|
+
|
179
235
|
// Backbone.Model
|
180
236
|
// --------------
|
181
237
|
|
182
|
-
//
|
238
|
+
// Backbone **Models** are the basic data object in the framework --
|
239
|
+
// frequently representing a row in a table in a database on your server.
|
240
|
+
// A discrete chunk of data and a bunch of useful, related methods for
|
241
|
+
// performing computations and transformations on that data.
|
242
|
+
|
243
|
+
// Create a new model with the specified attributes. A client id (`cid`)
|
183
244
|
// is automatically generated and assigned for you.
|
184
245
|
var Model = Backbone.Model = function(attributes, options) {
|
185
246
|
var defaults;
|
186
247
|
var attrs = attributes || {};
|
187
|
-
|
188
|
-
|
248
|
+
options || (options = {});
|
249
|
+
this.cid = _.uniqueId('c');
|
250
|
+
this.attributes = {};
|
251
|
+
_.extend(this, _.pick(options, modelOptions));
|
252
|
+
if (options.parse) attrs = this.parse(attrs, options) || {};
|
189
253
|
if (defaults = _.result(this, 'defaults')) {
|
190
|
-
attrs = _.
|
254
|
+
attrs = _.defaults({}, attrs, defaults);
|
191
255
|
}
|
192
|
-
this.
|
193
|
-
this._escapedAttributes = {};
|
194
|
-
this.cid = _.uniqueId('c');
|
195
|
-
this.changed = {};
|
196
|
-
this._changes = {};
|
197
|
-
this._pending = {};
|
198
|
-
this.set(attrs, {silent: true});
|
199
|
-
// Reset change tracking.
|
256
|
+
this.set(attrs, options);
|
200
257
|
this.changed = {};
|
201
|
-
this._changes = {};
|
202
|
-
this._pending = {};
|
203
|
-
this._previousAttributes = _.clone(this.attributes);
|
204
258
|
this.initialize.apply(this, arguments);
|
205
259
|
};
|
206
260
|
|
261
|
+
// A list of options to be attached directly to the model, if provided.
|
262
|
+
var modelOptions = ['url', 'urlRoot', 'collection'];
|
263
|
+
|
207
264
|
// Attach all inheritable methods to the Model prototype.
|
208
265
|
_.extend(Model.prototype, Events, {
|
209
266
|
|
210
267
|
// A hash of attributes whose current and previous value differ.
|
211
268
|
changed: null,
|
212
269
|
|
213
|
-
//
|
214
|
-
|
215
|
-
_changes: null,
|
216
|
-
|
217
|
-
// A hash of attributes that have changed since the last `change` event
|
218
|
-
// began.
|
219
|
-
_pending: null,
|
220
|
-
|
221
|
-
// A hash of attributes with the current model state to determine if
|
222
|
-
// a `change` should be recorded within a nested `change` block.
|
223
|
-
_changing : null,
|
270
|
+
// The value returned during the last failed validation.
|
271
|
+
validationError: null,
|
224
272
|
|
225
273
|
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
226
274
|
// CouchDB users may want to set this to `"_id"`.
|
@@ -235,7 +283,8 @@
|
|
235
283
|
return _.clone(this.attributes);
|
236
284
|
},
|
237
285
|
|
238
|
-
// Proxy `Backbone.sync` by default
|
286
|
+
// Proxy `Backbone.sync` by default -- but override this if you need
|
287
|
+
// custom syncing semantics for *this* particular model.
|
239
288
|
sync: function() {
|
240
289
|
return Backbone.sync.apply(this, arguments);
|
241
290
|
},
|
@@ -247,10 +296,7 @@
|
|
247
296
|
|
248
297
|
// Get the HTML-escaped value of an attribute.
|
249
298
|
escape: function(attr) {
|
250
|
-
|
251
|
-
if (html = this._escapedAttributes[attr]) return html;
|
252
|
-
var val = this.get(attr);
|
253
|
-
return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
|
299
|
+
return _.escape(this.get(attr));
|
254
300
|
},
|
255
301
|
|
256
302
|
// Returns `true` if the attribute contains a value that is not null
|
@@ -259,146 +305,194 @@
|
|
259
305
|
return this.get(attr) != null;
|
260
306
|
},
|
261
307
|
|
262
|
-
// Set a hash of model attributes on the object, firing `"change"
|
263
|
-
//
|
264
|
-
|
265
|
-
|
266
|
-
|
308
|
+
// Set a hash of model attributes on the object, firing `"change"`. This is
|
309
|
+
// the core primitive operation of a model, updating the data and notifying
|
310
|
+
// anyone who needs to know about the change in state. The heart of the beast.
|
311
|
+
set: function(key, val, options) {
|
312
|
+
var attr, attrs, unset, changes, silent, changing, prev, current;
|
313
|
+
if (key == null) return this;
|
267
314
|
|
268
315
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
269
|
-
if (
|
270
|
-
|
271
|
-
|
272
|
-
|
316
|
+
if (typeof key === 'object') {
|
317
|
+
attrs = key;
|
318
|
+
options = val;
|
319
|
+
} else {
|
320
|
+
(attrs = {})[key] = val;
|
273
321
|
}
|
274
322
|
|
275
|
-
|
276
|
-
var silent = options && options.silent;
|
277
|
-
var unset = options && options.unset;
|
278
|
-
if (attrs instanceof Model) attrs = attrs.attributes;
|
279
|
-
if (unset) for (attr in attrs) attrs[attr] = void 0;
|
323
|
+
options || (options = {});
|
280
324
|
|
281
325
|
// Run validation.
|
282
326
|
if (!this._validate(attrs, options)) return false;
|
283
327
|
|
328
|
+
// Extract attributes and options.
|
329
|
+
unset = options.unset;
|
330
|
+
silent = options.silent;
|
331
|
+
changes = [];
|
332
|
+
changing = this._changing;
|
333
|
+
this._changing = true;
|
334
|
+
|
335
|
+
if (!changing) {
|
336
|
+
this._previousAttributes = _.clone(this.attributes);
|
337
|
+
this.changed = {};
|
338
|
+
}
|
339
|
+
current = this.attributes, prev = this._previousAttributes;
|
340
|
+
|
284
341
|
// Check for changes of `id`.
|
285
342
|
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
286
343
|
|
287
|
-
|
288
|
-
var now = this.attributes;
|
289
|
-
var escaped = this._escapedAttributes;
|
290
|
-
var prev = this._previousAttributes || {};
|
291
|
-
|
292
|
-
// For each `set` attribute...
|
344
|
+
// For each `set` attribute, update or delete the current value.
|
293
345
|
for (attr in attrs) {
|
294
346
|
val = attrs[attr];
|
295
|
-
|
296
|
-
|
297
|
-
if (!_.isEqual(now[attr], val) || (unset && _.has(now, attr))) {
|
298
|
-
delete escaped[attr];
|
299
|
-
this._changes[attr] = true;
|
300
|
-
}
|
301
|
-
|
302
|
-
// Update or delete the current value.
|
303
|
-
unset ? delete now[attr] : now[attr] = val;
|
304
|
-
|
305
|
-
// If the new and previous value differ, record the change. If not,
|
306
|
-
// then remove changes for this attribute.
|
307
|
-
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) !== _.has(prev, attr))) {
|
347
|
+
if (!_.isEqual(current[attr], val)) changes.push(attr);
|
348
|
+
if (!_.isEqual(prev[attr], val)) {
|
308
349
|
this.changed[attr] = val;
|
309
|
-
if (!silent) this._pending[attr] = true;
|
310
350
|
} else {
|
311
351
|
delete this.changed[attr];
|
312
|
-
delete this._pending[attr];
|
313
|
-
if (!changing) delete this._changes[attr];
|
314
352
|
}
|
353
|
+
unset ? delete current[attr] : current[attr] = val;
|
354
|
+
}
|
315
355
|
|
316
|
-
|
356
|
+
// Trigger all relevant attribute changes.
|
357
|
+
if (!silent) {
|
358
|
+
if (changes.length) this._pending = true;
|
359
|
+
for (var i = 0, l = changes.length; i < l; i++) {
|
360
|
+
this.trigger('change:' + changes[i], this, current[changes[i]], options);
|
361
|
+
}
|
317
362
|
}
|
318
363
|
|
319
|
-
//
|
320
|
-
|
364
|
+
// You might be wondering why there's a `while` loop here. Changes can
|
365
|
+
// be recursively nested within `"change"` events.
|
366
|
+
if (changing) return this;
|
367
|
+
if (!silent) {
|
368
|
+
while (this._pending) {
|
369
|
+
this._pending = false;
|
370
|
+
this.trigger('change', this, options);
|
371
|
+
}
|
372
|
+
}
|
373
|
+
this._pending = false;
|
374
|
+
this._changing = false;
|
321
375
|
return this;
|
322
376
|
},
|
323
377
|
|
324
|
-
// Remove an attribute from the model, firing `"change"`
|
325
|
-
//
|
378
|
+
// Remove an attribute from the model, firing `"change"`. `unset` is a noop
|
379
|
+
// if the attribute doesn't exist.
|
326
380
|
unset: function(attr, options) {
|
327
|
-
|
328
|
-
return this.set(attr, null, options);
|
381
|
+
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
|
329
382
|
},
|
330
383
|
|
331
|
-
// Clear all attributes on the model, firing `"change"
|
332
|
-
// to silence it.
|
384
|
+
// Clear all attributes on the model, firing `"change"`.
|
333
385
|
clear: function(options) {
|
334
|
-
|
335
|
-
|
386
|
+
var attrs = {};
|
387
|
+
for (var key in this.attributes) attrs[key] = void 0;
|
388
|
+
return this.set(attrs, _.extend({}, options, {unset: true}));
|
389
|
+
},
|
390
|
+
|
391
|
+
// Determine if the model has changed since the last `"change"` event.
|
392
|
+
// If you specify an attribute name, determine if that attribute has changed.
|
393
|
+
hasChanged: function(attr) {
|
394
|
+
if (attr == null) return !_.isEmpty(this.changed);
|
395
|
+
return _.has(this.changed, attr);
|
396
|
+
},
|
397
|
+
|
398
|
+
// Return an object containing all the attributes that have changed, or
|
399
|
+
// false if there are no changed attributes. Useful for determining what
|
400
|
+
// parts of a view need to be updated and/or what attributes need to be
|
401
|
+
// persisted to the server. Unset attributes will be set to undefined.
|
402
|
+
// You can also pass an attributes object to diff against the model,
|
403
|
+
// determining if there *would be* a change.
|
404
|
+
changedAttributes: function(diff) {
|
405
|
+
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
406
|
+
var val, changed = false;
|
407
|
+
var old = this._changing ? this._previousAttributes : this.attributes;
|
408
|
+
for (var attr in diff) {
|
409
|
+
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
|
410
|
+
(changed || (changed = {}))[attr] = val;
|
411
|
+
}
|
412
|
+
return changed;
|
413
|
+
},
|
414
|
+
|
415
|
+
// Get the previous value of an attribute, recorded at the time the last
|
416
|
+
// `"change"` event was fired.
|
417
|
+
previous: function(attr) {
|
418
|
+
if (attr == null || !this._previousAttributes) return null;
|
419
|
+
return this._previousAttributes[attr];
|
420
|
+
},
|
421
|
+
|
422
|
+
// Get all of the attributes of the model at the time of the previous
|
423
|
+
// `"change"` event.
|
424
|
+
previousAttributes: function() {
|
425
|
+
return _.clone(this._previousAttributes);
|
336
426
|
},
|
337
427
|
|
338
428
|
// Fetch the model from the server. If the server's representation of the
|
339
|
-
// model differs from its current attributes, they will be
|
429
|
+
// model differs from its current attributes, they will be overridden,
|
340
430
|
// triggering a `"change"` event.
|
341
431
|
fetch: function(options) {
|
342
432
|
options = options ? _.clone(options) : {};
|
433
|
+
if (options.parse === void 0) options.parse = true;
|
343
434
|
var model = this;
|
344
435
|
var success = options.success;
|
345
|
-
options.success = function(resp
|
346
|
-
if (!model.set(model.parse(resp,
|
436
|
+
options.success = function(resp) {
|
437
|
+
if (!model.set(model.parse(resp, options), options)) return false;
|
347
438
|
if (success) success(model, resp, options);
|
439
|
+
model.trigger('sync', model, resp, options);
|
348
440
|
};
|
441
|
+
wrapError(this, options);
|
349
442
|
return this.sync('read', this, options);
|
350
443
|
},
|
351
444
|
|
352
445
|
// Set a hash of model attributes, and sync the model to the server.
|
353
446
|
// If the server returns an attributes hash that differs, the model's
|
354
447
|
// state will be `set` again.
|
355
|
-
save: function(
|
356
|
-
var
|
448
|
+
save: function(key, val, options) {
|
449
|
+
var attrs, method, xhr, attributes = this.attributes;
|
357
450
|
|
358
451
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
359
|
-
if (
|
360
|
-
|
361
|
-
|
362
|
-
|
452
|
+
if (key == null || typeof key === 'object') {
|
453
|
+
attrs = key;
|
454
|
+
options = val;
|
455
|
+
} else {
|
456
|
+
(attrs = {})[key] = val;
|
363
457
|
}
|
364
|
-
options = options ? _.clone(options) : {};
|
365
458
|
|
366
|
-
// If we're
|
367
|
-
if (options.wait)
|
368
|
-
if (!this._validate(attrs, options)) return false;
|
369
|
-
current = _.clone(this.attributes);
|
370
|
-
}
|
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;
|
371
461
|
|
372
|
-
|
373
|
-
var silentOptions = _.extend({}, options, {silent: true});
|
374
|
-
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
|
375
|
-
return false;
|
376
|
-
}
|
462
|
+
options = _.extend({validate: true}, options);
|
377
463
|
|
378
464
|
// Do not persist invalid models.
|
379
|
-
if (!
|
465
|
+
if (!this._validate(attrs, options)) return false;
|
466
|
+
|
467
|
+
// Set temporary attributes if `{wait: true}`.
|
468
|
+
if (attrs && options.wait) {
|
469
|
+
this.attributes = _.extend({}, attributes, attrs);
|
470
|
+
}
|
380
471
|
|
381
472
|
// After a successful server-side save, the client is (optionally)
|
382
473
|
// updated with the server-side state.
|
474
|
+
if (options.parse === void 0) options.parse = true;
|
383
475
|
var model = this;
|
384
476
|
var success = options.success;
|
385
|
-
options.success = function(resp
|
386
|
-
|
387
|
-
|
477
|
+
options.success = function(resp) {
|
478
|
+
// Ensure attributes are restored during synchronous saves.
|
479
|
+
model.attributes = attributes;
|
480
|
+
var serverAttrs = model.parse(resp, options);
|
388
481
|
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
|
389
|
-
if (!model.set(serverAttrs, options))
|
482
|
+
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
|
483
|
+
return false;
|
484
|
+
}
|
390
485
|
if (success) success(model, resp, options);
|
486
|
+
model.trigger('sync', model, resp, options);
|
391
487
|
};
|
488
|
+
wrapError(this, options);
|
392
489
|
|
393
|
-
|
394
|
-
|
490
|
+
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
|
491
|
+
if (method === 'patch') options.attrs = attrs;
|
492
|
+
xhr = this.sync(method, this, options);
|
395
493
|
|
396
|
-
//
|
397
|
-
|
398
|
-
if (!done && options.wait) {
|
399
|
-
this.clear(silentOptions);
|
400
|
-
this.set(current, silentOptions);
|
401
|
-
}
|
494
|
+
// Restore attributes.
|
495
|
+
if (attrs && options.wait) this.attributes = attributes;
|
402
496
|
|
403
497
|
return xhr;
|
404
498
|
},
|
@@ -418,12 +512,14 @@
|
|
418
512
|
options.success = function(resp) {
|
419
513
|
if (options.wait || model.isNew()) destroy();
|
420
514
|
if (success) success(model, resp, options);
|
515
|
+
if (!model.isNew()) model.trigger('sync', model, resp, options);
|
421
516
|
};
|
422
517
|
|
423
518
|
if (this.isNew()) {
|
424
519
|
options.success();
|
425
520
|
return false;
|
426
521
|
}
|
522
|
+
wrapError(this, options);
|
427
523
|
|
428
524
|
var xhr = this.sync('delete', this, options);
|
429
525
|
if (!options.wait) destroy();
|
@@ -441,7 +537,7 @@
|
|
441
537
|
|
442
538
|
// **parse** converts a response into the hash of attributes to be `set` on
|
443
539
|
// the model. The default implementation is just to pass the response along.
|
444
|
-
parse: function(resp,
|
540
|
+
parse: function(resp, options) {
|
445
541
|
return resp;
|
446
542
|
},
|
447
543
|
|
@@ -455,123 +551,63 @@
|
|
455
551
|
return this.id == null;
|
456
552
|
},
|
457
553
|
|
458
|
-
//
|
459
|
-
// a `"change:attribute"` event for each changed attribute.
|
460
|
-
// Calling this will cause all objects observing the model to update.
|
461
|
-
change: function(options) {
|
462
|
-
var changing = this._changing;
|
463
|
-
var current = this._changing = {};
|
464
|
-
|
465
|
-
// Silent changes become pending changes.
|
466
|
-
for (var attr in this._changes) this._pending[attr] = true;
|
467
|
-
|
468
|
-
// Trigger 'change:attr' for any new or silent changes.
|
469
|
-
var changes = this._changes;
|
470
|
-
this._changes = {};
|
471
|
-
|
472
|
-
// Set the correct state for this._changing values
|
473
|
-
var triggers = [];
|
474
|
-
for (var attr in changes) {
|
475
|
-
current[attr] = this.get(attr);
|
476
|
-
triggers.push(attr);
|
477
|
-
}
|
478
|
-
|
479
|
-
for (var i=0, l=triggers.length; i < l; i++) {
|
480
|
-
this.trigger('change:' + triggers[i], this, current[triggers[i]], options);
|
481
|
-
}
|
482
|
-
if (changing) return this;
|
483
|
-
|
484
|
-
// Continue firing `"change"` events while there are pending changes.
|
485
|
-
while (!_.isEmpty(this._pending)) {
|
486
|
-
this._pending = {};
|
487
|
-
this.trigger('change', this, options);
|
488
|
-
// Pending and silent changes still remain.
|
489
|
-
for (var attr in this.changed) {
|
490
|
-
if (this._pending[attr] || this._changes[attr]) continue;
|
491
|
-
delete this.changed[attr];
|
492
|
-
}
|
493
|
-
this._previousAttributes = _.clone(this.attributes);
|
494
|
-
}
|
495
|
-
|
496
|
-
this._changing = null;
|
497
|
-
return this;
|
498
|
-
},
|
499
|
-
|
500
|
-
// Determine if the model has changed since the last `"change"` event.
|
501
|
-
// If you specify an attribute name, determine if that attribute has changed.
|
502
|
-
hasChanged: function(attr) {
|
503
|
-
if (attr == null) return !_.isEmpty(this.changed);
|
504
|
-
return _.has(this.changed, attr);
|
505
|
-
},
|
506
|
-
|
507
|
-
// Return an object containing all the attributes that have changed, or
|
508
|
-
// false if there are no changed attributes. Useful for determining what
|
509
|
-
// parts of a view need to be updated and/or what attributes need to be
|
510
|
-
// persisted to the server. Unset attributes will be set to undefined.
|
511
|
-
// You can also pass an attributes object to diff against the model,
|
512
|
-
// determining if there *would be* a change.
|
513
|
-
changedAttributes: function(diff) {
|
514
|
-
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
515
|
-
var val, changed = false, old = this._previousAttributes;
|
516
|
-
for (var attr in diff) {
|
517
|
-
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
|
518
|
-
(changed || (changed = {}))[attr] = val;
|
519
|
-
}
|
520
|
-
return changed;
|
521
|
-
},
|
522
|
-
|
523
|
-
// Get the previous value of an attribute, recorded at the time the last
|
524
|
-
// `"change"` event was fired.
|
525
|
-
previous: function(attr) {
|
526
|
-
if (attr == null || !this._previousAttributes) return null;
|
527
|
-
return this._previousAttributes[attr];
|
528
|
-
},
|
529
|
-
|
530
|
-
// Get all of the attributes of the model at the time of the previous
|
531
|
-
// `"change"` event.
|
532
|
-
previousAttributes: function() {
|
533
|
-
return _.clone(this._previousAttributes);
|
534
|
-
},
|
535
|
-
|
536
|
-
// Check if the model is currently in a valid state. It's only possible to
|
537
|
-
// get into an *invalid* state if you're using silent changes.
|
554
|
+
// Check if the model is currently in a valid state.
|
538
555
|
isValid: function(options) {
|
539
|
-
return
|
556
|
+
return this._validate({}, _.extend(options || {}, { validate: true }));
|
540
557
|
},
|
541
558
|
|
542
559
|
// Run validation against the next complete set of model attributes,
|
543
|
-
// returning `true` if all is well.
|
544
|
-
// been passed, call that instead of firing the general `"error"` event.
|
560
|
+
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
|
545
561
|
_validate: function(attrs, options) {
|
546
|
-
if (options
|
562
|
+
if (!options.validate || !this.validate) return true;
|
547
563
|
attrs = _.extend({}, this.attributes, attrs);
|
548
|
-
var error = this.validate(attrs, options);
|
564
|
+
var error = this.validationError = this.validate(attrs, options) || null;
|
549
565
|
if (!error) return true;
|
550
|
-
|
551
|
-
this.trigger('error', this, error, options);
|
566
|
+
this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
|
552
567
|
return false;
|
553
568
|
}
|
554
569
|
|
555
570
|
});
|
556
571
|
|
572
|
+
// Underscore methods that we want to implement on the Model.
|
573
|
+
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
|
574
|
+
|
575
|
+
// Mix in each Underscore method as a proxy to `Model#attributes`.
|
576
|
+
_.each(modelMethods, function(method) {
|
577
|
+
Model.prototype[method] = function() {
|
578
|
+
var args = slice.call(arguments);
|
579
|
+
args.unshift(this.attributes);
|
580
|
+
return _[method].apply(_, args);
|
581
|
+
};
|
582
|
+
});
|
583
|
+
|
557
584
|
// Backbone.Collection
|
558
585
|
// -------------------
|
559
586
|
|
560
|
-
//
|
561
|
-
//
|
587
|
+
// If models tend to represent a single row of data, a Backbone Collection is
|
588
|
+
// more analagous to a table full of data ... or a small slice or page of that
|
589
|
+
// table, or a collection of rows that belong together for a particular reason
|
590
|
+
// -- all of the messages in this particular folder, all of the documents
|
591
|
+
// belonging to this particular author, and so on. Collections maintain
|
592
|
+
// indexes of their models, both in order, and for lookup by `id`.
|
593
|
+
|
594
|
+
// Create a new **Collection**, perhaps to contain a specific type of `model`.
|
595
|
+
// If a `comparator` is specified, the Collection will maintain
|
562
596
|
// its models in sort order, as they're added and removed.
|
563
597
|
var Collection = Backbone.Collection = function(models, options) {
|
564
598
|
options || (options = {});
|
599
|
+
if (options.url) this.url = options.url;
|
565
600
|
if (options.model) this.model = options.model;
|
566
601
|
if (options.comparator !== void 0) this.comparator = options.comparator;
|
567
602
|
this._reset();
|
568
603
|
this.initialize.apply(this, arguments);
|
569
|
-
if (models) {
|
570
|
-
if (options.parse) models = this.parse(models);
|
571
|
-
this.reset(models, {silent: true, parse: options.parse});
|
572
|
-
}
|
604
|
+
if (models) this.reset(models, _.extend({silent: true}, options));
|
573
605
|
};
|
574
606
|
|
607
|
+
// Default options for `Collection#set`.
|
608
|
+
var setOptions = {add: true, remove: true, merge: true};
|
609
|
+
var addOptions = {add: true, merge: false, remove: false};
|
610
|
+
|
575
611
|
// Define the Collection's inheritable methods.
|
576
612
|
_.extend(Collection.prototype, Events, {
|
577
613
|
|
@@ -594,87 +630,127 @@
|
|
594
630
|
return Backbone.sync.apply(this, arguments);
|
595
631
|
},
|
596
632
|
|
597
|
-
// Add a model, or list of models to the set.
|
598
|
-
// firing the `add` event for every new model.
|
633
|
+
// Add a model, or list of models to the set.
|
599
634
|
add: function(models, options) {
|
600
|
-
|
601
|
-
|
602
|
-
models = _.isArray(models) ? models.slice() : [models];
|
635
|
+
return this.set(models, _.defaults(options || {}, addOptions));
|
636
|
+
},
|
603
637
|
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
638
|
+
// Remove a model, or a list of models from the set.
|
639
|
+
remove: function(models, options) {
|
640
|
+
models = _.isArray(models) ? models.slice() : [models];
|
641
|
+
options || (options = {});
|
642
|
+
var i, l, index, model;
|
643
|
+
for (i = 0, l = models.length; i < l; i++) {
|
644
|
+
model = this.get(models[i]);
|
645
|
+
if (!model) continue;
|
646
|
+
delete this._byId[model.id];
|
647
|
+
delete this._byId[model.cid];
|
648
|
+
index = this.indexOf(model);
|
649
|
+
this.models.splice(index, 1);
|
650
|
+
this.length--;
|
651
|
+
if (!options.silent) {
|
652
|
+
options.index = index;
|
653
|
+
model.trigger('remove', model, this, options);
|
654
|
+
}
|
655
|
+
this._removeReference(model);
|
609
656
|
}
|
657
|
+
return this;
|
658
|
+
},
|
610
659
|
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
660
|
+
// Update a collection by `set`-ing a new list of models, adding new ones,
|
661
|
+
// removing models that are no longer present, and merging models that
|
662
|
+
// already exist in the collection, as necessary. Similar to **Model#set**,
|
663
|
+
// the core operation for updating the data contained by the collection.
|
664
|
+
set: function(models, options) {
|
665
|
+
options = _.defaults(options || {}, setOptions);
|
666
|
+
if (options.parse) models = this.parse(models, options);
|
667
|
+
if (!_.isArray(models)) models = models ? [models] : [];
|
668
|
+
var i, l, model, attrs, existing, sort;
|
669
|
+
var at = options.at;
|
670
|
+
var sortable = this.comparator && (at == null) && options.sort !== false;
|
671
|
+
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
|
672
|
+
var toAdd = [], toRemove = [], modelMap = {};
|
673
|
+
|
674
|
+
// Turn bare objects into model references, and prevent invalid models
|
675
|
+
// from being added.
|
676
|
+
for (i = 0, l = models.length; i < l; i++) {
|
677
|
+
if (!(model = this._prepareModel(models[i], options))) continue;
|
678
|
+
|
679
|
+
// If a duplicate is found, prevent it from being added and
|
680
|
+
// optionally merge it into the existing model.
|
681
|
+
if (existing = this.get(model)) {
|
682
|
+
if (options.remove) modelMap[existing.cid] = true;
|
683
|
+
if (options.merge) {
|
684
|
+
existing.set(model.attributes, options);
|
685
|
+
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
|
620
686
|
}
|
621
|
-
|
622
|
-
|
687
|
+
|
688
|
+
// This is a new model, push it to the `toAdd` list.
|
689
|
+
} else if (options.add) {
|
690
|
+
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;
|
623
697
|
}
|
698
|
+
}
|
624
699
|
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
700
|
+
// Remove nonexistent models if appropriate.
|
701
|
+
if (options.remove) {
|
702
|
+
for (i = 0, l = this.length; i < l; ++i) {
|
703
|
+
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
|
704
|
+
}
|
705
|
+
if (toRemove.length) this.remove(toRemove, options);
|
630
706
|
}
|
631
707
|
|
632
|
-
//
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
708
|
+
// See if sorting is needed, update `length` and splice in new models.
|
709
|
+
if (toAdd.length) {
|
710
|
+
if (sortable) sort = true;
|
711
|
+
this.length += toAdd.length;
|
712
|
+
if (at != null) {
|
713
|
+
splice.apply(this.models, [at, 0].concat(toAdd));
|
714
|
+
} else {
|
715
|
+
push.apply(this.models, toAdd);
|
716
|
+
}
|
717
|
+
}
|
637
718
|
|
638
|
-
//
|
639
|
-
if (
|
719
|
+
// Silently sort the collection if appropriate.
|
720
|
+
if (sort) this.sort({silent: true});
|
640
721
|
|
641
|
-
if (options
|
722
|
+
if (options.silent) return this;
|
642
723
|
|
643
724
|
// Trigger `add` events.
|
644
|
-
|
645
|
-
model.trigger('add', model, this, options);
|
725
|
+
for (i = 0, l = toAdd.length; i < l; i++) {
|
726
|
+
(model = toAdd[i]).trigger('add', model, this, options);
|
646
727
|
}
|
647
728
|
|
729
|
+
// Trigger `sort` if the collection was sorted.
|
730
|
+
if (sort) this.trigger('sort', this, options);
|
648
731
|
return this;
|
649
732
|
},
|
650
733
|
|
651
|
-
//
|
652
|
-
//
|
653
|
-
remove
|
654
|
-
|
734
|
+
// When you have more items than you want to add or remove individually,
|
735
|
+
// you can reset the entire set with a new list of models, without firing
|
736
|
+
// any granular `add` or `remove` events. Fires `reset` when finished.
|
737
|
+
// Useful for bulk operations and optimizations.
|
738
|
+
reset: function(models, options) {
|
655
739
|
options || (options = {});
|
656
|
-
|
657
|
-
|
658
|
-
model = this.getByCid(models[i]) || this.get(models[i]);
|
659
|
-
if (!model) continue;
|
660
|
-
delete this._byId[model.id];
|
661
|
-
delete this._byCid[model.cid];
|
662
|
-
index = this.indexOf(model);
|
663
|
-
this.models.splice(index, 1);
|
664
|
-
this.length--;
|
665
|
-
if (!options.silent) {
|
666
|
-
options.index = index;
|
667
|
-
model.trigger('remove', model, this, options);
|
668
|
-
}
|
669
|
-
this._removeReference(model);
|
740
|
+
for (var i = 0, l = this.models.length; i < l; i++) {
|
741
|
+
this._removeReference(this.models[i]);
|
670
742
|
}
|
743
|
+
options.previousModels = this.models;
|
744
|
+
this._reset();
|
745
|
+
this.add(models, _.extend({silent: true}, options));
|
746
|
+
if (!options.silent) this.trigger('reset', this, options);
|
671
747
|
return this;
|
672
748
|
},
|
673
749
|
|
674
750
|
// Add a model to the end of the collection.
|
675
751
|
push: function(model, options) {
|
676
752
|
model = this._prepareModel(model, options);
|
677
|
-
this.add(model, options);
|
753
|
+
this.add(model, _.extend({at: this.length}, options));
|
678
754
|
return model;
|
679
755
|
},
|
680
756
|
|
@@ -705,14 +781,9 @@
|
|
705
781
|
},
|
706
782
|
|
707
783
|
// Get a model from the set by id.
|
708
|
-
get: function(
|
709
|
-
if (
|
710
|
-
return this._byId[
|
711
|
-
},
|
712
|
-
|
713
|
-
// Get a model from the set by client id.
|
714
|
-
getByCid: function(cid) {
|
715
|
-
return cid && this._byCid[cid.cid || cid];
|
784
|
+
get: function(obj) {
|
785
|
+
if (obj == null) return void 0;
|
786
|
+
return this._byId[obj.id != null ? obj.id : obj.cid || obj];
|
716
787
|
},
|
717
788
|
|
718
789
|
// Get the model at the given index.
|
@@ -720,10 +791,11 @@
|
|
720
791
|
return this.models[index];
|
721
792
|
},
|
722
793
|
|
723
|
-
// Return models with matching attributes. Useful for simple cases of
|
724
|
-
|
725
|
-
|
726
|
-
|
794
|
+
// Return models with matching attributes. Useful for simple cases of
|
795
|
+
// `filter`.
|
796
|
+
where: function(attrs, first) {
|
797
|
+
if (_.isEmpty(attrs)) return first ? void 0 : [];
|
798
|
+
return this[first ? 'find' : 'filter'](function(model) {
|
727
799
|
for (var key in attrs) {
|
728
800
|
if (attrs[key] !== model.get(key)) return false;
|
729
801
|
}
|
@@ -731,54 +803,60 @@
|
|
731
803
|
});
|
732
804
|
},
|
733
805
|
|
806
|
+
// Return the first model with matching attributes. Useful for simple cases
|
807
|
+
// of `find`.
|
808
|
+
findWhere: function(attrs) {
|
809
|
+
return this.where(attrs, true);
|
810
|
+
},
|
811
|
+
|
734
812
|
// Force the collection to re-sort itself. You don't need to call this under
|
735
813
|
// normal circumstances, as the set will maintain sort order as each item
|
736
814
|
// is added.
|
737
815
|
sort: function(options) {
|
738
|
-
if (!this.comparator)
|
739
|
-
|
740
|
-
}
|
816
|
+
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
|
817
|
+
options || (options = {});
|
741
818
|
|
819
|
+
// Run sort based on type of `comparator`.
|
742
820
|
if (_.isString(this.comparator) || this.comparator.length === 1) {
|
743
821
|
this.models = this.sortBy(this.comparator, this);
|
744
822
|
} else {
|
745
823
|
this.models.sort(_.bind(this.comparator, this));
|
746
824
|
}
|
747
825
|
|
748
|
-
if (!options
|
826
|
+
if (!options.silent) this.trigger('sort', this, options);
|
749
827
|
return this;
|
750
828
|
},
|
751
829
|
|
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
|
+
|
752
840
|
// Pluck an attribute from each model in the collection.
|
753
841
|
pluck: function(attr) {
|
754
842
|
return _.invoke(this.models, 'get', attr);
|
755
843
|
},
|
756
844
|
|
757
|
-
// When you have more items than you want to add or remove individually,
|
758
|
-
// you can reset the entire set with a new list of models, without firing
|
759
|
-
// any `add` or `remove` events. Fires `reset` when finished.
|
760
|
-
reset: function(models, options) {
|
761
|
-
for (var i = 0, l = this.models.length; i < l; i++) {
|
762
|
-
this._removeReference(this.models[i]);
|
763
|
-
}
|
764
|
-
this._reset();
|
765
|
-
if (models) this.add(models, _.extend({silent: true}, options));
|
766
|
-
if (!options || !options.silent) this.trigger('reset', this, options);
|
767
|
-
return this;
|
768
|
-
},
|
769
|
-
|
770
845
|
// Fetch the default set of models for this collection, resetting the
|
771
|
-
// collection when they arrive. If `
|
772
|
-
//
|
846
|
+
// collection when they arrive. If `reset: true` is passed, the response
|
847
|
+
// data will be passed through the `reset` method instead of `set`.
|
773
848
|
fetch: function(options) {
|
774
849
|
options = options ? _.clone(options) : {};
|
775
850
|
if (options.parse === void 0) options.parse = true;
|
776
|
-
var collection = this;
|
777
851
|
var success = options.success;
|
778
|
-
|
779
|
-
|
852
|
+
var collection = this;
|
853
|
+
options.success = function(resp) {
|
854
|
+
var method = options.reset ? 'reset' : 'set';
|
855
|
+
collection[method](resp, options);
|
780
856
|
if (success) success(collection, resp, options);
|
857
|
+
collection.trigger('sync', collection, resp, options);
|
781
858
|
};
|
859
|
+
wrapError(this, options);
|
782
860
|
return this.sync('read', this, options);
|
783
861
|
},
|
784
862
|
|
@@ -786,13 +864,12 @@
|
|
786
864
|
// collection immediately, unless `wait: true` is passed, in which case we
|
787
865
|
// wait for the server to agree.
|
788
866
|
create: function(model, options) {
|
789
|
-
var collection = this;
|
790
867
|
options = options ? _.clone(options) : {};
|
791
|
-
model = this._prepareModel(model, options);
|
792
|
-
if (!
|
793
|
-
|
868
|
+
if (!(model = this._prepareModel(model, options))) return false;
|
869
|
+
if (!options.wait) this.add(model, options);
|
870
|
+
var collection = this;
|
794
871
|
var success = options.success;
|
795
|
-
options.success = function(
|
872
|
+
options.success = function(resp) {
|
796
873
|
if (options.wait) collection.add(model, options);
|
797
874
|
if (success) success(model, resp, options);
|
798
875
|
};
|
@@ -802,7 +879,7 @@
|
|
802
879
|
|
803
880
|
// **parse** converts a response into a list of models to be added to the
|
804
881
|
// collection. The default implementation is just to pass it through.
|
805
|
-
parse: function(resp,
|
882
|
+
parse: function(resp, options) {
|
806
883
|
return resp;
|
807
884
|
},
|
808
885
|
|
@@ -811,22 +888,16 @@
|
|
811
888
|
return new this.constructor(this.models);
|
812
889
|
},
|
813
890
|
|
814
|
-
//
|
815
|
-
//
|
816
|
-
|
817
|
-
chain: function() {
|
818
|
-
return _(this.models).chain();
|
819
|
-
},
|
820
|
-
|
821
|
-
// Reset all internal state. Called when the collection is reset.
|
822
|
-
_reset: function(options) {
|
891
|
+
// Private method to reset all internal state. Called when the collection
|
892
|
+
// is first initialized or reset.
|
893
|
+
_reset: function() {
|
823
894
|
this.length = 0;
|
824
895
|
this.models = [];
|
825
896
|
this._byId = {};
|
826
|
-
this._byCid = {};
|
827
897
|
},
|
828
898
|
|
829
|
-
// Prepare a
|
899
|
+
// Prepare a hash of attributes (or other model) to be added to this
|
900
|
+
// collection.
|
830
901
|
_prepareModel: function(attrs, options) {
|
831
902
|
if (attrs instanceof Model) {
|
832
903
|
if (!attrs.collection) attrs.collection = this;
|
@@ -835,11 +906,14 @@
|
|
835
906
|
options || (options = {});
|
836
907
|
options.collection = this;
|
837
908
|
var model = new this.model(attrs, options);
|
838
|
-
if (!model._validate(
|
909
|
+
if (!model._validate(attrs, options)) {
|
910
|
+
this.trigger('invalid', this, attrs, options);
|
911
|
+
return false;
|
912
|
+
}
|
839
913
|
return model;
|
840
914
|
},
|
841
915
|
|
842
|
-
// Internal method to
|
916
|
+
// Internal method to sever a model's ties to a collection.
|
843
917
|
_removeReference: function(model) {
|
844
918
|
if (this === model.collection) delete model.collection;
|
845
919
|
model.off('all', this._onModelEvent, this);
|
@@ -862,12 +936,14 @@
|
|
862
936
|
});
|
863
937
|
|
864
938
|
// Underscore methods that we want to implement on the Collection.
|
939
|
+
// 90% of the core usefulness of Backbone Collections is actually implemented
|
940
|
+
// right here:
|
865
941
|
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
|
866
942
|
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
|
867
943
|
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
|
868
|
-
'max', 'min', '
|
869
|
-
'
|
870
|
-
'
|
944
|
+
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
|
945
|
+
'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
|
946
|
+
'isEmpty', 'chain'];
|
871
947
|
|
872
948
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
873
949
|
_.each(methods, function(method) {
|
@@ -891,8 +967,243 @@
|
|
891
967
|
};
|
892
968
|
});
|
893
969
|
|
970
|
+
// Backbone.View
|
971
|
+
// -------------
|
972
|
+
|
973
|
+
// Backbone Views are almost more convention than they are actual code. A View
|
974
|
+
// is simply a JavaScript object that represents a logical chunk of UI in the
|
975
|
+
// DOM. This might be a single item, an entire list, a sidebar or panel, or
|
976
|
+
// even the surrounding frame which wraps your whole app. Defining a chunk of
|
977
|
+
// UI as a **View** allows you to define your DOM events declaratively, without
|
978
|
+
// having to worry about render order ... and makes it easy for the view to
|
979
|
+
// react to specific changes in the state of your models.
|
980
|
+
|
981
|
+
// Creating a Backbone.View creates its initial element outside of the DOM,
|
982
|
+
// if an existing element is not provided...
|
983
|
+
var View = Backbone.View = function(options) {
|
984
|
+
this.cid = _.uniqueId('view');
|
985
|
+
this._configure(options || {});
|
986
|
+
this._ensureElement();
|
987
|
+
this.initialize.apply(this, arguments);
|
988
|
+
this.delegateEvents();
|
989
|
+
};
|
990
|
+
|
991
|
+
// Cached regex to split keys for `delegate`.
|
992
|
+
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
993
|
+
|
994
|
+
// List of view options to be merged as properties.
|
995
|
+
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
|
996
|
+
|
997
|
+
// Set up all inheritable **Backbone.View** properties and methods.
|
998
|
+
_.extend(View.prototype, Events, {
|
999
|
+
|
1000
|
+
// The default `tagName` of a View's element is `"div"`.
|
1001
|
+
tagName: 'div',
|
1002
|
+
|
1003
|
+
// jQuery delegate for element lookup, scoped to DOM elements within the
|
1004
|
+
// current view. This should be prefered to global lookups where possible.
|
1005
|
+
$: function(selector) {
|
1006
|
+
return this.$el.find(selector);
|
1007
|
+
},
|
1008
|
+
|
1009
|
+
// Initialize is an empty function by default. Override it with your own
|
1010
|
+
// initialization logic.
|
1011
|
+
initialize: function(){},
|
1012
|
+
|
1013
|
+
// **render** is the core function that your view should override, in order
|
1014
|
+
// to populate its element (`this.el`), with the appropriate HTML. The
|
1015
|
+
// convention is for **render** to always return `this`.
|
1016
|
+
render: function() {
|
1017
|
+
return this;
|
1018
|
+
},
|
1019
|
+
|
1020
|
+
// Remove this view by taking the element out of the DOM, and removing any
|
1021
|
+
// applicable Backbone.Events listeners.
|
1022
|
+
remove: function() {
|
1023
|
+
this.$el.remove();
|
1024
|
+
this.stopListening();
|
1025
|
+
return this;
|
1026
|
+
},
|
1027
|
+
|
1028
|
+
// Change the view's element (`this.el` property), including event
|
1029
|
+
// re-delegation.
|
1030
|
+
setElement: function(element, delegate) {
|
1031
|
+
if (this.$el) this.undelegateEvents();
|
1032
|
+
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
|
1033
|
+
this.el = this.$el[0];
|
1034
|
+
if (delegate !== false) this.delegateEvents();
|
1035
|
+
return this;
|
1036
|
+
},
|
1037
|
+
|
1038
|
+
// Set callbacks, where `this.events` is a hash of
|
1039
|
+
//
|
1040
|
+
// *{"event selector": "callback"}*
|
1041
|
+
//
|
1042
|
+
// {
|
1043
|
+
// 'mousedown .title': 'edit',
|
1044
|
+
// 'click .button': 'save'
|
1045
|
+
// 'click .open': function(e) { ... }
|
1046
|
+
// }
|
1047
|
+
//
|
1048
|
+
// pairs. Callbacks will be bound to the view, with `this` set properly.
|
1049
|
+
// Uses event delegation for efficiency.
|
1050
|
+
// Omitting the selector binds the event to `this.el`.
|
1051
|
+
// This only works for delegate-able events: not `focus`, `blur`, and
|
1052
|
+
// not `change`, `submit`, and `reset` in Internet Explorer.
|
1053
|
+
delegateEvents: function(events) {
|
1054
|
+
if (!(events || (events = _.result(this, 'events')))) return this;
|
1055
|
+
this.undelegateEvents();
|
1056
|
+
for (var key in events) {
|
1057
|
+
var method = events[key];
|
1058
|
+
if (!_.isFunction(method)) method = this[events[key]];
|
1059
|
+
if (!method) continue;
|
1060
|
+
|
1061
|
+
var match = key.match(delegateEventSplitter);
|
1062
|
+
var eventName = match[1], selector = match[2];
|
1063
|
+
method = _.bind(method, this);
|
1064
|
+
eventName += '.delegateEvents' + this.cid;
|
1065
|
+
if (selector === '') {
|
1066
|
+
this.$el.on(eventName, method);
|
1067
|
+
} else {
|
1068
|
+
this.$el.on(eventName, selector, method);
|
1069
|
+
}
|
1070
|
+
}
|
1071
|
+
return this;
|
1072
|
+
},
|
1073
|
+
|
1074
|
+
// Clears all callbacks previously bound to the view with `delegateEvents`.
|
1075
|
+
// You usually don't need to use this, but may wish to if you have multiple
|
1076
|
+
// Backbone views attached to the same DOM element.
|
1077
|
+
undelegateEvents: function() {
|
1078
|
+
this.$el.off('.delegateEvents' + this.cid);
|
1079
|
+
return this;
|
1080
|
+
},
|
1081
|
+
|
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
|
+
// Ensure that the View has a DOM element to render into.
|
1093
|
+
// If `this.el` is a string, pass it through `$()`, take the first
|
1094
|
+
// matching element, and re-assign it to `el`. Otherwise, create
|
1095
|
+
// an element from the `id`, `className` and `tagName` properties.
|
1096
|
+
_ensureElement: function() {
|
1097
|
+
if (!this.el) {
|
1098
|
+
var attrs = _.extend({}, _.result(this, 'attributes'));
|
1099
|
+
if (this.id) attrs.id = _.result(this, 'id');
|
1100
|
+
if (this.className) attrs['class'] = _.result(this, 'className');
|
1101
|
+
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
|
1102
|
+
this.setElement($el, false);
|
1103
|
+
} else {
|
1104
|
+
this.setElement(_.result(this, 'el'), false);
|
1105
|
+
}
|
1106
|
+
}
|
1107
|
+
|
1108
|
+
});
|
1109
|
+
|
1110
|
+
// Backbone.sync
|
1111
|
+
// -------------
|
1112
|
+
|
1113
|
+
// Override this function to change the manner in which Backbone persists
|
1114
|
+
// models to the server. You will be passed the type of request, and the
|
1115
|
+
// model in question. By default, makes a RESTful Ajax request
|
1116
|
+
// to the model's `url()`. Some possible customizations could be:
|
1117
|
+
//
|
1118
|
+
// * Use `setTimeout` to batch rapid-fire updates into a single request.
|
1119
|
+
// * Send up the models as XML instead of JSON.
|
1120
|
+
// * Persist models via WebSockets instead of Ajax.
|
1121
|
+
//
|
1122
|
+
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
|
1123
|
+
// as `POST`, with a `_method` parameter containing the true HTTP method,
|
1124
|
+
// as well as all requests with the body as `application/x-www-form-urlencoded`
|
1125
|
+
// instead of `application/json` with the model in a param named `model`.
|
1126
|
+
// Useful when interfacing with server-side languages like **PHP** that make
|
1127
|
+
// it difficult to read the body of `PUT` requests.
|
1128
|
+
Backbone.sync = function(method, model, options) {
|
1129
|
+
var type = methodMap[method];
|
1130
|
+
|
1131
|
+
// Default options, unless specified.
|
1132
|
+
_.defaults(options || (options = {}), {
|
1133
|
+
emulateHTTP: Backbone.emulateHTTP,
|
1134
|
+
emulateJSON: Backbone.emulateJSON
|
1135
|
+
});
|
1136
|
+
|
1137
|
+
// Default JSON-request options.
|
1138
|
+
var params = {type: type, dataType: 'json'};
|
1139
|
+
|
1140
|
+
// Ensure that we have a URL.
|
1141
|
+
if (!options.url) {
|
1142
|
+
params.url = _.result(model, 'url') || urlError();
|
1143
|
+
}
|
1144
|
+
|
1145
|
+
// Ensure that we have the appropriate request data.
|
1146
|
+
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
1147
|
+
params.contentType = 'application/json';
|
1148
|
+
params.data = JSON.stringify(options.attrs || model.toJSON(options));
|
1149
|
+
}
|
1150
|
+
|
1151
|
+
// For older servers, emulate JSON by encoding the request into an HTML-form.
|
1152
|
+
if (options.emulateJSON) {
|
1153
|
+
params.contentType = 'application/x-www-form-urlencoded';
|
1154
|
+
params.data = params.data ? {model: params.data} : {};
|
1155
|
+
}
|
1156
|
+
|
1157
|
+
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
|
1158
|
+
// And an `X-HTTP-Method-Override` header.
|
1159
|
+
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
|
1160
|
+
params.type = 'POST';
|
1161
|
+
if (options.emulateJSON) params.data._method = type;
|
1162
|
+
var beforeSend = options.beforeSend;
|
1163
|
+
options.beforeSend = function(xhr) {
|
1164
|
+
xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
1165
|
+
if (beforeSend) return beforeSend.apply(this, arguments);
|
1166
|
+
};
|
1167
|
+
}
|
1168
|
+
|
1169
|
+
// Don't process data on a non-GET request.
|
1170
|
+
if (params.type !== 'GET' && !options.emulateJSON) {
|
1171
|
+
params.processData = false;
|
1172
|
+
}
|
1173
|
+
|
1174
|
+
// If we're sending a `PATCH` request, and we're in an old Internet Explorer
|
1175
|
+
// that still has ActiveX enabled by default, override jQuery to use that
|
1176
|
+
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
|
1177
|
+
if (params.type === 'PATCH' && window.ActiveXObject &&
|
1178
|
+
!(window.external && window.external.msActiveXFilteringEnabled)) {
|
1179
|
+
params.xhr = function() {
|
1180
|
+
return new ActiveXObject("Microsoft.XMLHTTP");
|
1181
|
+
};
|
1182
|
+
}
|
1183
|
+
|
1184
|
+
// Make the request, allowing the user to override any Ajax options.
|
1185
|
+
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
|
1186
|
+
model.trigger('request', model, xhr, options);
|
1187
|
+
return xhr;
|
1188
|
+
};
|
1189
|
+
|
1190
|
+
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
1191
|
+
var methodMap = {
|
1192
|
+
'create': 'POST',
|
1193
|
+
'update': 'PUT',
|
1194
|
+
'patch': 'PATCH',
|
1195
|
+
'delete': 'DELETE',
|
1196
|
+
'read': 'GET'
|
1197
|
+
};
|
1198
|
+
|
1199
|
+
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
|
1200
|
+
// Override this if you'd like to use a different library.
|
1201
|
+
Backbone.ajax = function() {
|
1202
|
+
return Backbone.$.ajax.apply(Backbone.$, arguments);
|
1203
|
+
};
|
1204
|
+
|
894
1205
|
// Backbone.Router
|
895
|
-
//
|
1206
|
+
// ---------------
|
896
1207
|
|
897
1208
|
// Routers map faux-URLs to actions, and fire events when routes are
|
898
1209
|
// matched. Creating a new one sets its `routes` hash, if not set statically.
|
@@ -906,9 +1217,9 @@
|
|
906
1217
|
// Cached regular expressions for matching named param parts and splatted
|
907
1218
|
// parts of route strings.
|
908
1219
|
var optionalParam = /\((.*?)\)/g;
|
909
|
-
var namedParam =
|
1220
|
+
var namedParam = /(\(\?)?:\w+/g;
|
910
1221
|
var splatParam = /\*\w+/g;
|
911
|
-
var escapeRegExp = /[
|
1222
|
+
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
|
912
1223
|
|
913
1224
|
// Set up all inheritable **Backbone.Router** properties and methods.
|
914
1225
|
_.extend(Router.prototype, Events, {
|
@@ -925,13 +1236,19 @@
|
|
925
1236
|
//
|
926
1237
|
route: function(route, name, callback) {
|
927
1238
|
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
1239
|
+
if (_.isFunction(name)) {
|
1240
|
+
callback = name;
|
1241
|
+
name = '';
|
1242
|
+
}
|
928
1243
|
if (!callback) callback = this[name];
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
1244
|
+
var router = this;
|
1245
|
+
Backbone.history.route(route, function(fragment) {
|
1246
|
+
var args = router._extractParameters(route, fragment);
|
1247
|
+
callback && callback.apply(router, args);
|
1248
|
+
router.trigger.apply(router, ['route:' + name].concat(args));
|
1249
|
+
router.trigger('route', name, args);
|
1250
|
+
Backbone.history.trigger('route', router, name, args);
|
1251
|
+
});
|
935
1252
|
return this;
|
936
1253
|
},
|
937
1254
|
|
@@ -946,12 +1263,10 @@
|
|
946
1263
|
// routes can be defined at the bottom of the route map.
|
947
1264
|
_bindRoutes: function() {
|
948
1265
|
if (!this.routes) return;
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
for (var i = 0, l = routes.length; i < l; i++) {
|
954
|
-
this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
|
1266
|
+
this.routes = _.result(this, 'routes');
|
1267
|
+
var route, routes = _.keys(this.routes);
|
1268
|
+
while ((route = routes.pop()) != null) {
|
1269
|
+
this.route(route, this.routes[route]);
|
955
1270
|
}
|
956
1271
|
},
|
957
1272
|
|
@@ -960,15 +1275,21 @@
|
|
960
1275
|
_routeToRegExp: function(route) {
|
961
1276
|
route = route.replace(escapeRegExp, '\\$&')
|
962
1277
|
.replace(optionalParam, '(?:$1)?')
|
963
|
-
.replace(namedParam,
|
1278
|
+
.replace(namedParam, function(match, optional){
|
1279
|
+
return optional ? match : '([^\/]+)';
|
1280
|
+
})
|
964
1281
|
.replace(splatParam, '(.*?)');
|
965
1282
|
return new RegExp('^' + route + '$');
|
966
1283
|
},
|
967
1284
|
|
968
1285
|
// Given a route, and a URL fragment that it matches, return the array of
|
969
|
-
// extracted parameters.
|
1286
|
+
// extracted decoded parameters. Empty or unmatched parameters will be
|
1287
|
+
// treated as `null` to normalize cross-browser behavior.
|
970
1288
|
_extractParameters: function(route, fragment) {
|
971
|
-
|
1289
|
+
var params = route.exec(fragment).slice(1);
|
1290
|
+
return _.map(params, function(param) {
|
1291
|
+
return param ? decodeURIComponent(param) : null;
|
1292
|
+
});
|
972
1293
|
}
|
973
1294
|
|
974
1295
|
});
|
@@ -976,21 +1297,24 @@
|
|
976
1297
|
// Backbone.History
|
977
1298
|
// ----------------
|
978
1299
|
|
979
|
-
// Handles cross-browser history management, based on
|
980
|
-
//
|
1300
|
+
// Handles cross-browser history management, based on either
|
1301
|
+
// [pushState](http://diveintohtml5.info/history.html) and real URLs, or
|
1302
|
+
// [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
|
1303
|
+
// and URL fragments. If the browser supports neither (old IE, natch),
|
1304
|
+
// falls back to polling.
|
981
1305
|
var History = Backbone.History = function() {
|
982
1306
|
this.handlers = [];
|
983
1307
|
_.bindAll(this, 'checkUrl');
|
984
1308
|
|
985
|
-
//
|
1309
|
+
// Ensure that `History` can be used outside of the browser.
|
986
1310
|
if (typeof window !== 'undefined') {
|
987
1311
|
this.location = window.location;
|
988
1312
|
this.history = window.history;
|
989
1313
|
}
|
990
1314
|
};
|
991
1315
|
|
992
|
-
// Cached regex for
|
993
|
-
var routeStripper = /^[#\/]
|
1316
|
+
// Cached regex for stripping a leading hash/slash and trailing space.
|
1317
|
+
var routeStripper = /^[#\/]|\s+$/g;
|
994
1318
|
|
995
1319
|
// Cached regex for stripping leading and trailing slashes.
|
996
1320
|
var rootStripper = /^\/+|\/+$/g;
|
@@ -1030,7 +1354,7 @@
|
|
1030
1354
|
fragment = this.getHash();
|
1031
1355
|
}
|
1032
1356
|
}
|
1033
|
-
return
|
1357
|
+
return fragment.replace(routeStripper, '');
|
1034
1358
|
},
|
1035
1359
|
|
1036
1360
|
// Start the hash change handling, returning `true` if the current URL matches
|
@@ -1061,9 +1385,9 @@
|
|
1061
1385
|
// Depending on whether we're using pushState or hashes, and whether
|
1062
1386
|
// 'onhashchange' is supported, determine how we check the URL state.
|
1063
1387
|
if (this._hasPushState) {
|
1064
|
-
Backbone.$(window).
|
1388
|
+
Backbone.$(window).on('popstate', this.checkUrl);
|
1065
1389
|
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
|
1066
|
-
Backbone.$(window).
|
1390
|
+
Backbone.$(window).on('hashchange', this.checkUrl);
|
1067
1391
|
} else if (this._wantsHashChange) {
|
1068
1392
|
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
|
1069
1393
|
}
|
@@ -1095,7 +1419,7 @@
|
|
1095
1419
|
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
|
1096
1420
|
// but possibly useful for unit testing Routers.
|
1097
1421
|
stop: function() {
|
1098
|
-
Backbone.$(window).
|
1422
|
+
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
|
1099
1423
|
clearInterval(this._checkUrlInterval);
|
1100
1424
|
History.started = false;
|
1101
1425
|
},
|
@@ -1178,7 +1502,7 @@
|
|
1178
1502
|
var href = location.href.replace(/(javascript:|#).*$/, '');
|
1179
1503
|
location.replace(href + '#' + fragment);
|
1180
1504
|
} else {
|
1181
|
-
//
|
1505
|
+
// Some browsers require that `hash` contains a leading #.
|
1182
1506
|
location.hash = '#' + fragment;
|
1183
1507
|
}
|
1184
1508
|
}
|
@@ -1188,247 +1512,6 @@
|
|
1188
1512
|
// Create the default Backbone.history.
|
1189
1513
|
Backbone.history = new History;
|
1190
1514
|
|
1191
|
-
// Backbone.View
|
1192
|
-
// -------------
|
1193
|
-
|
1194
|
-
// Creating a Backbone.View creates its initial element outside of the DOM,
|
1195
|
-
// if an existing element is not provided...
|
1196
|
-
var View = Backbone.View = function(options) {
|
1197
|
-
this.cid = _.uniqueId('view');
|
1198
|
-
this._configure(options || {});
|
1199
|
-
this._ensureElement();
|
1200
|
-
this.initialize.apply(this, arguments);
|
1201
|
-
this.delegateEvents();
|
1202
|
-
};
|
1203
|
-
|
1204
|
-
// Cached regex to split keys for `delegate`.
|
1205
|
-
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
1206
|
-
|
1207
|
-
// List of view options to be merged as properties.
|
1208
|
-
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
|
1209
|
-
|
1210
|
-
// Set up all inheritable **Backbone.View** properties and methods.
|
1211
|
-
_.extend(View.prototype, Events, {
|
1212
|
-
|
1213
|
-
// The default `tagName` of a View's element is `"div"`.
|
1214
|
-
tagName: 'div',
|
1215
|
-
|
1216
|
-
// jQuery delegate for element lookup, scoped to DOM elements within the
|
1217
|
-
// current view. This should be prefered to global lookups where possible.
|
1218
|
-
$: function(selector) {
|
1219
|
-
return this.$el.find(selector);
|
1220
|
-
},
|
1221
|
-
|
1222
|
-
// Initialize is an empty function by default. Override it with your own
|
1223
|
-
// initialization logic.
|
1224
|
-
initialize: function(){},
|
1225
|
-
|
1226
|
-
// **render** is the core function that your view should override, in order
|
1227
|
-
// to populate its element (`this.el`), with the appropriate HTML. The
|
1228
|
-
// convention is for **render** to always return `this`.
|
1229
|
-
render: function() {
|
1230
|
-
return this;
|
1231
|
-
},
|
1232
|
-
|
1233
|
-
// Clean up references to this view in order to prevent latent effects and
|
1234
|
-
// memory leaks.
|
1235
|
-
dispose: function() {
|
1236
|
-
this.undelegateEvents();
|
1237
|
-
if (this.model && this.model.off) this.model.off(null, null, this);
|
1238
|
-
if (this.collection && this.collection.off) this.collection.off(null, null, this);
|
1239
|
-
return this;
|
1240
|
-
},
|
1241
|
-
|
1242
|
-
// Remove this view from the DOM. Note that the view isn't present in the
|
1243
|
-
// DOM by default, so calling this method may be a no-op.
|
1244
|
-
remove: function() {
|
1245
|
-
this.dispose();
|
1246
|
-
this.$el.remove();
|
1247
|
-
return this;
|
1248
|
-
},
|
1249
|
-
|
1250
|
-
// For small amounts of DOM Elements, where a full-blown template isn't
|
1251
|
-
// needed, use **make** to manufacture elements, one at a time.
|
1252
|
-
//
|
1253
|
-
// var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
|
1254
|
-
//
|
1255
|
-
make: function(tagName, attributes, content) {
|
1256
|
-
var el = document.createElement(tagName);
|
1257
|
-
if (attributes) Backbone.$(el).attr(attributes);
|
1258
|
-
if (content != null) Backbone.$(el).html(content);
|
1259
|
-
return el;
|
1260
|
-
},
|
1261
|
-
|
1262
|
-
// Change the view's element (`this.el` property), including event
|
1263
|
-
// re-delegation.
|
1264
|
-
setElement: function(element, delegate) {
|
1265
|
-
if (this.$el) this.undelegateEvents();
|
1266
|
-
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
|
1267
|
-
this.el = this.$el[0];
|
1268
|
-
if (delegate !== false) this.delegateEvents();
|
1269
|
-
return this;
|
1270
|
-
},
|
1271
|
-
|
1272
|
-
// Set callbacks, where `this.events` is a hash of
|
1273
|
-
//
|
1274
|
-
// *{"event selector": "callback"}*
|
1275
|
-
//
|
1276
|
-
// {
|
1277
|
-
// 'mousedown .title': 'edit',
|
1278
|
-
// 'click .button': 'save'
|
1279
|
-
// 'click .open': function(e) { ... }
|
1280
|
-
// }
|
1281
|
-
//
|
1282
|
-
// pairs. Callbacks will be bound to the view, with `this` set properly.
|
1283
|
-
// Uses event delegation for efficiency.
|
1284
|
-
// Omitting the selector binds the event to `this.el`.
|
1285
|
-
// This only works for delegate-able events: not `focus`, `blur`, and
|
1286
|
-
// not `change`, `submit`, and `reset` in Internet Explorer.
|
1287
|
-
delegateEvents: function(events) {
|
1288
|
-
if (!(events || (events = _.result(this, 'events')))) return;
|
1289
|
-
this.undelegateEvents();
|
1290
|
-
for (var key in events) {
|
1291
|
-
var method = events[key];
|
1292
|
-
if (!_.isFunction(method)) method = this[events[key]];
|
1293
|
-
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
|
1294
|
-
var match = key.match(delegateEventSplitter);
|
1295
|
-
var eventName = match[1], selector = match[2];
|
1296
|
-
method = _.bind(method, this);
|
1297
|
-
eventName += '.delegateEvents' + this.cid;
|
1298
|
-
if (selector === '') {
|
1299
|
-
this.$el.bind(eventName, method);
|
1300
|
-
} else {
|
1301
|
-
this.$el.delegate(selector, eventName, method);
|
1302
|
-
}
|
1303
|
-
}
|
1304
|
-
},
|
1305
|
-
|
1306
|
-
// Clears all callbacks previously bound to the view with `delegateEvents`.
|
1307
|
-
// You usually don't need to use this, but may wish to if you have multiple
|
1308
|
-
// Backbone views attached to the same DOM element.
|
1309
|
-
undelegateEvents: function() {
|
1310
|
-
this.$el.unbind('.delegateEvents' + this.cid);
|
1311
|
-
},
|
1312
|
-
|
1313
|
-
// Performs the initial configuration of a View with a set of options.
|
1314
|
-
// Keys with special meaning *(model, collection, id, className)*, are
|
1315
|
-
// attached directly to the view.
|
1316
|
-
_configure: function(options) {
|
1317
|
-
if (this.options) options = _.extend({}, this.options, options);
|
1318
|
-
for (var i = 0, l = viewOptions.length; i < l; i++) {
|
1319
|
-
var attr = viewOptions[i];
|
1320
|
-
if (options[attr]) this[attr] = options[attr];
|
1321
|
-
}
|
1322
|
-
this.options = options;
|
1323
|
-
},
|
1324
|
-
|
1325
|
-
// Ensure that the View has a DOM element to render into.
|
1326
|
-
// If `this.el` is a string, pass it through `$()`, take the first
|
1327
|
-
// matching element, and re-assign it to `el`. Otherwise, create
|
1328
|
-
// an element from the `id`, `className` and `tagName` properties.
|
1329
|
-
_ensureElement: function() {
|
1330
|
-
if (!this.el) {
|
1331
|
-
var attrs = _.extend({}, _.result(this, 'attributes'));
|
1332
|
-
if (this.id) attrs.id = _.result(this, 'id');
|
1333
|
-
if (this.className) attrs['class'] = _.result(this, 'className');
|
1334
|
-
this.setElement(this.make(_.result(this, 'tagName'), attrs), false);
|
1335
|
-
} else {
|
1336
|
-
this.setElement(_.result(this, 'el'), false);
|
1337
|
-
}
|
1338
|
-
}
|
1339
|
-
|
1340
|
-
});
|
1341
|
-
|
1342
|
-
// Backbone.sync
|
1343
|
-
// -------------
|
1344
|
-
|
1345
|
-
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
1346
|
-
var methodMap = {
|
1347
|
-
'create': 'POST',
|
1348
|
-
'update': 'PUT',
|
1349
|
-
'delete': 'DELETE',
|
1350
|
-
'read': 'GET'
|
1351
|
-
};
|
1352
|
-
|
1353
|
-
// Override this function to change the manner in which Backbone persists
|
1354
|
-
// models to the server. You will be passed the type of request, and the
|
1355
|
-
// model in question. By default, makes a RESTful Ajax request
|
1356
|
-
// to the model's `url()`. Some possible customizations could be:
|
1357
|
-
//
|
1358
|
-
// * Use `setTimeout` to batch rapid-fire updates into a single request.
|
1359
|
-
// * Send up the models as XML instead of JSON.
|
1360
|
-
// * Persist models via WebSockets instead of Ajax.
|
1361
|
-
//
|
1362
|
-
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
|
1363
|
-
// as `POST`, with a `_method` parameter containing the true HTTP method,
|
1364
|
-
// as well as all requests with the body as `application/x-www-form-urlencoded`
|
1365
|
-
// instead of `application/json` with the model in a param named `model`.
|
1366
|
-
// Useful when interfacing with server-side languages like **PHP** that make
|
1367
|
-
// it difficult to read the body of `PUT` requests.
|
1368
|
-
Backbone.sync = function(method, model, options) {
|
1369
|
-
var type = methodMap[method];
|
1370
|
-
|
1371
|
-
// Default options, unless specified.
|
1372
|
-
options || (options = {});
|
1373
|
-
|
1374
|
-
// Default JSON-request options.
|
1375
|
-
var params = {type: type, dataType: 'json'};
|
1376
|
-
|
1377
|
-
// Ensure that we have a URL.
|
1378
|
-
if (!options.url) {
|
1379
|
-
params.url = _.result(model, 'url') || urlError();
|
1380
|
-
}
|
1381
|
-
|
1382
|
-
// Ensure that we have the appropriate request data.
|
1383
|
-
if (!options.data && model && (method === 'create' || method === 'update')) {
|
1384
|
-
params.contentType = 'application/json';
|
1385
|
-
params.data = JSON.stringify(model);
|
1386
|
-
}
|
1387
|
-
|
1388
|
-
// For older servers, emulate JSON by encoding the request into an HTML-form.
|
1389
|
-
if (Backbone.emulateJSON) {
|
1390
|
-
params.contentType = 'application/x-www-form-urlencoded';
|
1391
|
-
params.data = params.data ? {model: params.data} : {};
|
1392
|
-
}
|
1393
|
-
|
1394
|
-
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
|
1395
|
-
// And an `X-HTTP-Method-Override` header.
|
1396
|
-
if (Backbone.emulateHTTP) {
|
1397
|
-
if (type === 'PUT' || type === 'DELETE') {
|
1398
|
-
if (Backbone.emulateJSON) params.data._method = type;
|
1399
|
-
params.type = 'POST';
|
1400
|
-
params.beforeSend = function(xhr) {
|
1401
|
-
xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
1402
|
-
};
|
1403
|
-
}
|
1404
|
-
}
|
1405
|
-
|
1406
|
-
// Don't process data on a non-GET request.
|
1407
|
-
if (params.type !== 'GET' && !Backbone.emulateJSON) {
|
1408
|
-
params.processData = false;
|
1409
|
-
}
|
1410
|
-
|
1411
|
-
var success = options.success;
|
1412
|
-
options.success = function(resp, status, xhr) {
|
1413
|
-
if (success) success(resp, status, xhr);
|
1414
|
-
model.trigger('sync', model, resp, options);
|
1415
|
-
};
|
1416
|
-
|
1417
|
-
var error = options.error;
|
1418
|
-
options.error = function(xhr, status, thrown) {
|
1419
|
-
if (error) error(model, xhr, options);
|
1420
|
-
model.trigger('error', model, xhr, options);
|
1421
|
-
};
|
1422
|
-
|
1423
|
-
// Make the request, allowing the user to override any Ajax options.
|
1424
|
-
return Backbone.ajax(_.extend(params, options));
|
1425
|
-
};
|
1426
|
-
|
1427
|
-
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
|
1428
|
-
Backbone.ajax = function() {
|
1429
|
-
return Backbone.$.ajax.apply(Backbone.$, arguments);
|
1430
|
-
};
|
1431
|
-
|
1432
1515
|
// Helpers
|
1433
1516
|
// -------
|
1434
1517
|
|
@@ -1445,7 +1528,7 @@
|
|
1445
1528
|
if (protoProps && _.has(protoProps, 'constructor')) {
|
1446
1529
|
child = protoProps.constructor;
|
1447
1530
|
} else {
|
1448
|
-
child = function(){ parent.apply(this, arguments); };
|
1531
|
+
child = function(){ return parent.apply(this, arguments); };
|
1449
1532
|
}
|
1450
1533
|
|
1451
1534
|
// Add static properties to the constructor function, if supplied.
|
@@ -1468,12 +1551,21 @@
|
|
1468
1551
|
return child;
|
1469
1552
|
};
|
1470
1553
|
|
1471
|
-
// Set up inheritance for the model, collection, router, and
|
1472
|
-
Model.extend = Collection.extend = Router.extend = View.extend = extend;
|
1554
|
+
// Set up inheritance for the model, collection, router, view and history.
|
1555
|
+
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
|
1473
1556
|
|
1474
1557
|
// Throw an error when a URL is needed, and none is supplied.
|
1475
1558
|
var urlError = function() {
|
1476
1559
|
throw new Error('A "url" property or function must be specified');
|
1477
1560
|
};
|
1478
1561
|
|
1562
|
+
// Wrap an optional error callback with a fallback error event.
|
1563
|
+
var wrapError = function (model, options) {
|
1564
|
+
var error = options.error;
|
1565
|
+
options.error = function(resp) {
|
1566
|
+
if (error) error(model, resp, options);
|
1567
|
+
model.trigger('error', model, resp, options);
|
1568
|
+
};
|
1569
|
+
};
|
1570
|
+
|
1479
1571
|
}).call(this);
|