bone_tree 0.5.6 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +26 -3
- data/Gemfile +5 -12
- data/Rakefile +7 -9
- data/bone_tree.gemspec +30 -14
- data/config.rb +8 -0
- data/lib/assets/javascripts/bone_tree.js +277 -981
- data/lib/assets/stylesheets/bone_tree.css +108 -0
- data/lib/bone_tree/rails.rb +5 -0
- data/lib/bone_tree/sprockets.rb +3 -0
- data/lib/{version.rb → bone_tree/version.rb} +1 -1
- data/lib/bone_tree.rb +11 -6
- data/source/index.html.haml +10 -382
- data/source/javascripts/{_backbone.js → backbone.js} +282 -142
- data/source/javascripts/bone_tree/models/directory.js.coffee +109 -0
- data/source/javascripts/bone_tree/models/file.js.coffee +12 -0
- data/source/javascripts/bone_tree/models/node.js.coffee +24 -0
- data/source/javascripts/bone_tree/models/settings.js.coffee +5 -0
- data/source/javascripts/bone_tree/{_namespace.js.coffee → namespace.js.coffee} +1 -1
- data/source/javascripts/bone_tree/views/directory.js.coffee +56 -0
- data/source/javascripts/bone_tree/views/file.js.coffee +25 -0
- data/source/javascripts/bone_tree/views/tree.js.coffee +87 -0
- data/source/javascripts/bone_tree.js.coffee +2 -1
- data/source/stylesheets/bone_tree.css.sass +0 -38
- data/spec/javascripts/directory_spec.coffee +26 -0
- data/spec/javascripts/helpers/spec_helper.coffee +3 -4
- data/spec/javascripts/sorting_spec.coffee +12 -0
- data/spec/javascripts/support/jasmine.yml +6 -4
- metadata +143 -36
- data/Gemfile.lock +0 -190
- data/docs/index.html +0 -222
- data/docs/resources/base.css +0 -70
- data/docs/resources/index.css +0 -20
- data/docs/resources/module.css +0 -24
- data/docs/source/javascripts/bone_tree/_namespace.js.html +0 -45
- data/docs/source/javascripts/bone_tree/models/_directory.js.html +0 -126
- data/docs/source/javascripts/bone_tree/models/_file.js.html +0 -112
- data/docs/source/javascripts/bone_tree/models/_nodes.js.html +0 -174
- data/docs/source/javascripts/bone_tree/models/_settings.js.html +0 -75
- data/docs/source/javascripts/bone_tree/views/_directory.js.html +0 -94
- data/docs/source/javascripts/bone_tree/views/_file.js.html +0 -82
- data/docs/source/javascripts/bone_tree/views/_menu.js.html +0 -110
- data/docs/source/javascripts/bone_tree/views/_tree.js.html +0 -432
- data/source/javascripts/_jquery.min.js +0 -5
- data/source/javascripts/_underscore.js +0 -999
- data/source/javascripts/bone_tree/models/_directory.js.coffee +0 -63
- data/source/javascripts/bone_tree/models/_file.js.coffee +0 -55
- data/source/javascripts/bone_tree/models/_nodes.js.coffee +0 -117
- data/source/javascripts/bone_tree/models/_settings.js.coffee +0 -25
- data/source/javascripts/bone_tree/views/_directory.js.coffee +0 -73
- data/source/javascripts/bone_tree/views/_file.js.coffee +0 -51
- data/source/javascripts/bone_tree/views/_menu.js.coffee +0 -97
- data/source/javascripts/bone_tree/views/_tree.js.coffee +0 -498
- data/spec/javascripts/directory_view_spec.coffee +0 -91
- data/spec/javascripts/file_view_spec.coffee +0 -70
- data/spec/javascripts/menu_view_spec.coffee +0 -42
- data/spec/javascripts/nodes_spec.coffee +0 -37
- data/spec/javascripts/tree_view_spec.coffee +0 -39
@@ -1,7 +1,6 @@
|
|
1
|
-
//= require
|
2
|
-
//= require
|
3
|
-
//
|
4
|
-
// Backbone.js 0.9.1
|
1
|
+
//= require underscore
|
2
|
+
//= require jquery
|
3
|
+
// Backbone.js 0.9.2
|
5
4
|
|
6
5
|
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
7
6
|
// Backbone may be freely distributed under the MIT license.
|
@@ -35,7 +34,7 @@
|
|
35
34
|
}
|
36
35
|
|
37
36
|
// Current version of the library. Keep in sync with `package.json`.
|
38
|
-
Backbone.VERSION = '0.9.
|
37
|
+
Backbone.VERSION = '0.9.2';
|
39
38
|
|
40
39
|
// Require Underscore, if we're on the server, and it's not already present.
|
41
40
|
var _ = root._;
|
@@ -74,6 +73,9 @@
|
|
74
73
|
// Backbone.Events
|
75
74
|
// -----------------
|
76
75
|
|
76
|
+
// Regular expression used to split event strings
|
77
|
+
var eventSplitter = /\s+/;
|
78
|
+
|
77
79
|
// A module that can be mixed in to *any object* in order to provide it with
|
78
80
|
// custom events. You may bind with `on` or remove with `off` callback functions
|
79
81
|
// to an event; trigger`-ing an event fires all callbacks in succession.
|
@@ -83,89 +85,110 @@
|
|
83
85
|
// object.on('expand', function(){ alert('expanded'); });
|
84
86
|
// object.trigger('expand');
|
85
87
|
//
|
86
|
-
Backbone.Events = {
|
88
|
+
var Events = Backbone.Events = {
|
87
89
|
|
88
|
-
// Bind
|
90
|
+
// Bind one or more space separated events, `events`, to a `callback`
|
89
91
|
// function. Passing `"all"` will bind the callback to all events fired.
|
90
92
|
on: function(events, callback, context) {
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
93
|
+
|
94
|
+
var calls, event, node, tail, list;
|
95
|
+
if (!callback) return this;
|
96
|
+
events = events.split(eventSplitter);
|
97
|
+
calls = this._callbacks || (this._callbacks = {});
|
98
|
+
|
99
|
+
// Create an immutable callback list, allowing traversal during
|
100
|
+
// modification. The tail is an empty object that will always be used
|
101
|
+
// as the next node.
|
102
|
+
while (event = events.shift()) {
|
103
|
+
list = calls[event];
|
104
|
+
node = list ? list.tail : {};
|
105
|
+
node.next = tail = {};
|
106
|
+
node.context = context;
|
107
|
+
node.callback = callback;
|
108
|
+
calls[event] = {tail: tail, next: list ? list.next : node};
|
103
109
|
}
|
110
|
+
|
104
111
|
return this;
|
105
112
|
},
|
106
113
|
|
107
114
|
// Remove one or many callbacks. If `context` is null, removes all callbacks
|
108
115
|
// with that function. If `callback` is null, removes all callbacks for the
|
109
|
-
// event. If `
|
116
|
+
// event. If `events` is null, removes all bound callbacks for all events.
|
110
117
|
off: function(events, callback, context) {
|
111
|
-
var
|
112
|
-
|
118
|
+
var event, calls, node, tail, cb, ctx;
|
119
|
+
|
120
|
+
// No events, or removing *all* events.
|
121
|
+
if (!(calls = this._callbacks)) return;
|
122
|
+
if (!(events || callback || context)) {
|
113
123
|
delete this._callbacks;
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
124
|
+
return this;
|
125
|
+
}
|
126
|
+
|
127
|
+
// Loop through the listed events and contexts, splicing them out of the
|
128
|
+
// linked list of callbacks if appropriate.
|
129
|
+
events = events ? events.split(eventSplitter) : _.keys(calls);
|
130
|
+
while (event = events.shift()) {
|
131
|
+
node = calls[event];
|
132
|
+
delete calls[event];
|
133
|
+
if (!node || !(callback || context)) continue;
|
134
|
+
// Create a new list, omitting the indicated callbacks.
|
135
|
+
tail = node.tail;
|
136
|
+
while ((node = node.next) !== tail) {
|
137
|
+
cb = node.callback;
|
138
|
+
ctx = node.context;
|
139
|
+
if ((callback && cb !== callback) || (context && ctx !== context)) {
|
140
|
+
this.on(event, cb, ctx);
|
125
141
|
}
|
126
142
|
}
|
127
143
|
}
|
144
|
+
|
128
145
|
return this;
|
129
146
|
},
|
130
147
|
|
131
|
-
// Trigger
|
132
|
-
// same arguments as `trigger` is, apart from the event name
|
133
|
-
//
|
148
|
+
// Trigger one or many events, firing all bound callbacks. Callbacks are
|
149
|
+
// passed the same arguments as `trigger` is, apart from the event name
|
150
|
+
// (unless you're listening on `"all"`, which will cause your callback to
|
151
|
+
// receive the true name of the event as the first argument).
|
134
152
|
trigger: function(events) {
|
135
153
|
var event, node, calls, tail, args, all, rest;
|
136
154
|
if (!(calls = this._callbacks)) return this;
|
137
|
-
all = calls
|
138
|
-
|
139
|
-
// Save references to the current heads & tails.
|
140
|
-
while (event = events.shift()) {
|
141
|
-
if (all) events.push({next: all.next, tail: all.tail, event: event});
|
142
|
-
if (!(node = calls[event])) continue;
|
143
|
-
events.push({next: node.next, tail: node.tail});
|
144
|
-
}
|
145
|
-
// Traverse each list, stopping when the saved tail is reached.
|
155
|
+
all = calls.all;
|
156
|
+
events = events.split(eventSplitter);
|
146
157
|
rest = slice.call(arguments, 1);
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
158
|
+
|
159
|
+
// For each event, walk through the linked list of callbacks twice,
|
160
|
+
// first to trigger the event, then to trigger any `"all"` callbacks.
|
161
|
+
while (event = events.shift()) {
|
162
|
+
if (node = calls[event]) {
|
163
|
+
tail = node.tail;
|
164
|
+
while ((node = node.next) !== tail) {
|
165
|
+
node.callback.apply(node.context || this, rest);
|
166
|
+
}
|
167
|
+
}
|
168
|
+
if (node = all) {
|
169
|
+
tail = node.tail;
|
170
|
+
args = [event].concat(rest);
|
171
|
+
while ((node = node.next) !== tail) {
|
172
|
+
node.callback.apply(node.context || this, args);
|
173
|
+
}
|
152
174
|
}
|
153
175
|
}
|
176
|
+
|
154
177
|
return this;
|
155
178
|
}
|
156
179
|
|
157
180
|
};
|
158
181
|
|
159
182
|
// Aliases for backwards compatibility.
|
160
|
-
|
161
|
-
|
183
|
+
Events.bind = Events.on;
|
184
|
+
Events.unbind = Events.off;
|
162
185
|
|
163
186
|
// Backbone.Model
|
164
187
|
// --------------
|
165
188
|
|
166
189
|
// Create a new model, with defined attributes. A client id (`cid`)
|
167
190
|
// is automatically generated and assigned for you.
|
168
|
-
Backbone.Model = function(attributes, options) {
|
191
|
+
var Model = Backbone.Model = function(attributes, options) {
|
169
192
|
var defaults;
|
170
193
|
attributes || (attributes = {});
|
171
194
|
if (options && options.parse) attributes = this.parse(attributes);
|
@@ -176,16 +199,31 @@
|
|
176
199
|
this.attributes = {};
|
177
200
|
this._escapedAttributes = {};
|
178
201
|
this.cid = _.uniqueId('c');
|
179
|
-
|
180
|
-
|
181
|
-
}
|
182
|
-
|
202
|
+
this.changed = {};
|
203
|
+
this._silent = {};
|
204
|
+
this._pending = {};
|
205
|
+
this.set(attributes, {silent: true});
|
206
|
+
// Reset change tracking.
|
207
|
+
this.changed = {};
|
208
|
+
this._silent = {};
|
209
|
+
this._pending = {};
|
183
210
|
this._previousAttributes = _.clone(this.attributes);
|
184
211
|
this.initialize.apply(this, arguments);
|
185
212
|
};
|
186
213
|
|
187
214
|
// Attach all inheritable methods to the Model prototype.
|
188
|
-
_.extend(
|
215
|
+
_.extend(Model.prototype, Events, {
|
216
|
+
|
217
|
+
// A hash of attributes whose current and previous value differ.
|
218
|
+
changed: null,
|
219
|
+
|
220
|
+
// A hash of attributes that have silently changed since the last time
|
221
|
+
// `change` was called. Will become pending attributes on the next call.
|
222
|
+
_silent: null,
|
223
|
+
|
224
|
+
// A hash of attributes that have changed since the last `'change'` event
|
225
|
+
// began.
|
226
|
+
_pending: null,
|
189
227
|
|
190
228
|
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
191
229
|
// CouchDB users may want to set this to `"_id"`.
|
@@ -196,7 +234,7 @@
|
|
196
234
|
initialize: function(){},
|
197
235
|
|
198
236
|
// Return a copy of the model's `attributes` object.
|
199
|
-
toJSON: function() {
|
237
|
+
toJSON: function(options) {
|
200
238
|
return _.clone(this.attributes);
|
201
239
|
},
|
202
240
|
|
@@ -209,20 +247,22 @@
|
|
209
247
|
escape: function(attr) {
|
210
248
|
var html;
|
211
249
|
if (html = this._escapedAttributes[attr]) return html;
|
212
|
-
var val = this.
|
250
|
+
var val = this.get(attr);
|
213
251
|
return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
|
214
252
|
},
|
215
253
|
|
216
254
|
// Returns `true` if the attribute contains a value that is not null
|
217
255
|
// or undefined.
|
218
256
|
has: function(attr) {
|
219
|
-
return this.
|
257
|
+
return this.get(attr) != null;
|
220
258
|
},
|
221
259
|
|
222
260
|
// Set a hash of model attributes on the object, firing `"change"` unless
|
223
261
|
// you choose to silence it.
|
224
262
|
set: function(key, value, options) {
|
225
263
|
var attrs, attr, val;
|
264
|
+
|
265
|
+
// Handle both `"key", value` and `{key: value}` -style arguments.
|
226
266
|
if (_.isObject(key) || key == null) {
|
227
267
|
attrs = key;
|
228
268
|
options = value;
|
@@ -234,7 +274,7 @@
|
|
234
274
|
// Extract attributes and options.
|
235
275
|
options || (options = {});
|
236
276
|
if (!attrs) return this;
|
237
|
-
if (attrs instanceof
|
277
|
+
if (attrs instanceof Model) attrs = attrs.attributes;
|
238
278
|
if (options.unset) for (attr in attrs) attrs[attr] = void 0;
|
239
279
|
|
240
280
|
// Run validation.
|
@@ -243,33 +283,37 @@
|
|
243
283
|
// Check for changes of `id`.
|
244
284
|
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
245
285
|
|
286
|
+
var changes = options.changes = {};
|
246
287
|
var now = this.attributes;
|
247
288
|
var escaped = this._escapedAttributes;
|
248
289
|
var prev = this._previousAttributes || {};
|
249
|
-
var alreadySetting = this._setting;
|
250
|
-
this._changed || (this._changed = {});
|
251
|
-
this._setting = true;
|
252
290
|
|
253
|
-
//
|
291
|
+
// For each `set` attribute...
|
254
292
|
for (attr in attrs) {
|
255
293
|
val = attrs[attr];
|
256
|
-
|
257
|
-
|
258
|
-
if (
|
259
|
-
|
260
|
-
this.
|
294
|
+
|
295
|
+
// If the new and current value differ, record the change.
|
296
|
+
if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
|
297
|
+
delete escaped[attr];
|
298
|
+
(options.silent ? this._silent : changes)[attr] = true;
|
261
299
|
}
|
262
|
-
|
300
|
+
|
301
|
+
// Update or delete the current value.
|
302
|
+
options.unset ? delete now[attr] : now[attr] = val;
|
303
|
+
|
304
|
+
// If the new and previous value differ, record the change. If not,
|
305
|
+
// then remove changes for this attribute.
|
263
306
|
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
|
264
|
-
this.
|
307
|
+
this.changed[attr] = val;
|
308
|
+
if (!options.silent) this._pending[attr] = true;
|
309
|
+
} else {
|
310
|
+
delete this.changed[attr];
|
311
|
+
delete this._pending[attr];
|
265
312
|
}
|
266
313
|
}
|
267
314
|
|
268
|
-
// Fire the `"change"` events
|
269
|
-
if (!
|
270
|
-
if (!options.silent && this.hasChanged()) this.change(options);
|
271
|
-
this._setting = false;
|
272
|
-
}
|
315
|
+
// Fire the `"change"` events.
|
316
|
+
if (!options.silent) this.change(options);
|
273
317
|
return this;
|
274
318
|
},
|
275
319
|
|
@@ -307,6 +351,8 @@
|
|
307
351
|
// state will be `set` again.
|
308
352
|
save: function(key, value, options) {
|
309
353
|
var attrs, current;
|
354
|
+
|
355
|
+
// Handle both `("key", value)` and `({key: value})` -style calls.
|
310
356
|
if (_.isObject(key) || key == null) {
|
311
357
|
attrs = key;
|
312
358
|
options = value;
|
@@ -314,18 +360,30 @@
|
|
314
360
|
attrs = {};
|
315
361
|
attrs[key] = value;
|
316
362
|
}
|
317
|
-
|
318
363
|
options = options ? _.clone(options) : {};
|
319
|
-
|
364
|
+
|
365
|
+
// If we're "wait"-ing to set changed attributes, validate early.
|
366
|
+
if (options.wait) {
|
367
|
+
if (!this._validate(attrs, options)) return false;
|
368
|
+
current = _.clone(this.attributes);
|
369
|
+
}
|
370
|
+
|
371
|
+
// Regular saves `set` attributes before persisting to the server.
|
320
372
|
var silentOptions = _.extend({}, options, {silent: true});
|
321
373
|
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
|
322
374
|
return false;
|
323
375
|
}
|
376
|
+
|
377
|
+
// After a successful server-side save, the client is (optionally)
|
378
|
+
// updated with the server-side state.
|
324
379
|
var model = this;
|
325
380
|
var success = options.success;
|
326
381
|
options.success = function(resp, status, xhr) {
|
327
382
|
var serverAttrs = model.parse(resp, xhr);
|
328
|
-
if (options.wait)
|
383
|
+
if (options.wait) {
|
384
|
+
delete options.wait;
|
385
|
+
serverAttrs = _.extend(attrs || {}, serverAttrs);
|
386
|
+
}
|
329
387
|
if (!model.set(serverAttrs, options)) return false;
|
330
388
|
if (success) {
|
331
389
|
success(model, resp);
|
@@ -333,6 +391,8 @@
|
|
333
391
|
model.trigger('sync', model, resp, options);
|
334
392
|
}
|
335
393
|
};
|
394
|
+
|
395
|
+
// Finish configuring and sending the Ajax request.
|
336
396
|
options.error = Backbone.wrapError(options.error, model, options);
|
337
397
|
var method = this.isNew() ? 'create' : 'update';
|
338
398
|
var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
|
@@ -352,7 +412,11 @@
|
|
352
412
|
model.trigger('destroy', model, model.collection, options);
|
353
413
|
};
|
354
414
|
|
355
|
-
if (this.isNew())
|
415
|
+
if (this.isNew()) {
|
416
|
+
triggerDestroy();
|
417
|
+
return false;
|
418
|
+
}
|
419
|
+
|
356
420
|
options.success = function(resp) {
|
357
421
|
if (options.wait) triggerDestroy();
|
358
422
|
if (success) {
|
@@ -361,6 +425,7 @@
|
|
361
425
|
model.trigger('sync', model, resp, options);
|
362
426
|
}
|
363
427
|
};
|
428
|
+
|
364
429
|
options.error = Backbone.wrapError(options.error, model, options);
|
365
430
|
var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
|
366
431
|
if (!options.wait) triggerDestroy();
|
@@ -371,7 +436,7 @@
|
|
371
436
|
// using Backbone's restful methods, override this to change the endpoint
|
372
437
|
// that will be called.
|
373
438
|
url: function() {
|
374
|
-
var base = getValue(this
|
439
|
+
var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
|
375
440
|
if (this.isNew()) return base;
|
376
441
|
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
|
377
442
|
},
|
@@ -396,18 +461,33 @@
|
|
396
461
|
// a `"change:attribute"` event for each changed attribute.
|
397
462
|
// Calling this will cause all objects observing the model to update.
|
398
463
|
change: function(options) {
|
399
|
-
|
464
|
+
options || (options = {});
|
465
|
+
var changing = this._changing;
|
400
466
|
this._changing = true;
|
401
|
-
|
402
|
-
|
403
|
-
|
467
|
+
|
468
|
+
// Silent changes become pending changes.
|
469
|
+
for (var attr in this._silent) this._pending[attr] = true;
|
470
|
+
|
471
|
+
// Silent changes are triggered.
|
472
|
+
var changes = _.extend({}, options.changes, this._silent);
|
473
|
+
this._silent = {};
|
474
|
+
for (var attr in changes) {
|
475
|
+
this.trigger('change:' + attr, this, this.get(attr), options);
|
404
476
|
}
|
405
|
-
|
406
|
-
|
477
|
+
if (changing) return this;
|
478
|
+
|
479
|
+
// Continue firing `"change"` events while there are pending changes.
|
480
|
+
while (!_.isEmpty(this._pending)) {
|
481
|
+
this._pending = {};
|
407
482
|
this.trigger('change', this, options);
|
483
|
+
// Pending and silent changes still remain.
|
484
|
+
for (var attr in this.changed) {
|
485
|
+
if (this._pending[attr] || this._silent[attr]) continue;
|
486
|
+
delete this.changed[attr];
|
487
|
+
}
|
488
|
+
this._previousAttributes = _.clone(this.attributes);
|
408
489
|
}
|
409
|
-
|
410
|
-
delete this._changed;
|
490
|
+
|
411
491
|
this._changing = false;
|
412
492
|
return this;
|
413
493
|
},
|
@@ -415,8 +495,8 @@
|
|
415
495
|
// Determine if the model has changed since the last `"change"` event.
|
416
496
|
// If you specify an attribute name, determine if that attribute has changed.
|
417
497
|
hasChanged: function(attr) {
|
418
|
-
if (!arguments.length) return !_.isEmpty(this.
|
419
|
-
return
|
498
|
+
if (!arguments.length) return !_.isEmpty(this.changed);
|
499
|
+
return _.has(this.changed, attr);
|
420
500
|
},
|
421
501
|
|
422
502
|
// Return an object containing all the attributes that have changed, or
|
@@ -426,7 +506,7 @@
|
|
426
506
|
// You can also pass an attributes object to diff against the model,
|
427
507
|
// determining if there *would be* a change.
|
428
508
|
changedAttributes: function(diff) {
|
429
|
-
if (!diff) return this.hasChanged() ? _.clone(this.
|
509
|
+
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
430
510
|
var val, changed = false, old = this._previousAttributes;
|
431
511
|
for (var attr in diff) {
|
432
512
|
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
|
@@ -454,9 +534,9 @@
|
|
454
534
|
return !this.validate(this.attributes);
|
455
535
|
},
|
456
536
|
|
457
|
-
// Run validation against
|
458
|
-
// if all is well. If a specific `error` callback has
|
459
|
-
// call that instead of firing the general `"error"` event.
|
537
|
+
// Run validation against the next complete set of model attributes,
|
538
|
+
// returning `true` if all is well. If a specific `error` callback has
|
539
|
+
// been passed, call that instead of firing the general `"error"` event.
|
460
540
|
_validate: function(attrs, options) {
|
461
541
|
if (options.silent || !this.validate) return true;
|
462
542
|
attrs = _.extend({}, this.attributes, attrs);
|
@@ -478,8 +558,9 @@
|
|
478
558
|
// Provides a standard collection class for our sets of models, ordered
|
479
559
|
// or unordered. If a `comparator` is specified, the Collection will maintain
|
480
560
|
// its models in sort order, as they're added and removed.
|
481
|
-
Backbone.Collection = function(models, options) {
|
561
|
+
var Collection = Backbone.Collection = function(models, options) {
|
482
562
|
options || (options = {});
|
563
|
+
if (options.model) this.model = options.model;
|
483
564
|
if (options.comparator) this.comparator = options.comparator;
|
484
565
|
this._reset();
|
485
566
|
this.initialize.apply(this, arguments);
|
@@ -487,11 +568,11 @@
|
|
487
568
|
};
|
488
569
|
|
489
570
|
// Define the Collection's inheritable methods.
|
490
|
-
_.extend(
|
571
|
+
_.extend(Collection.prototype, Events, {
|
491
572
|
|
492
573
|
// The default model for a collection is just a **Backbone.Model**.
|
493
574
|
// This should be overridden in most cases.
|
494
|
-
model:
|
575
|
+
model: Model,
|
495
576
|
|
496
577
|
// Initialize is an empty function by default. Override it with your own
|
497
578
|
// initialization logic.
|
@@ -499,14 +580,14 @@
|
|
499
580
|
|
500
581
|
// The JSON representation of a Collection is an array of the
|
501
582
|
// models' attributes.
|
502
|
-
toJSON: function() {
|
503
|
-
return this.map(function(model){ return model.toJSON(); });
|
583
|
+
toJSON: function(options) {
|
584
|
+
return this.map(function(model){ return model.toJSON(options); });
|
504
585
|
},
|
505
586
|
|
506
587
|
// Add a model, or list of models to the set. Pass **silent** to avoid
|
507
588
|
// firing the `add` event for every new model.
|
508
589
|
add: function(models, options) {
|
509
|
-
var i, index, length, model, cid, id, cids = {}, ids = {};
|
590
|
+
var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
|
510
591
|
options || (options = {});
|
511
592
|
models = _.isArray(models) ? models.slice() : [models];
|
512
593
|
|
@@ -516,16 +597,24 @@
|
|
516
597
|
if (!(model = models[i] = this._prepareModel(models[i], options))) {
|
517
598
|
throw new Error("Can't add an invalid model to a collection");
|
518
599
|
}
|
519
|
-
|
520
|
-
|
521
|
-
|
600
|
+
cid = model.cid;
|
601
|
+
id = model.id;
|
602
|
+
if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
|
603
|
+
dups.push(i);
|
604
|
+
continue;
|
522
605
|
}
|
523
606
|
cids[cid] = ids[id] = model;
|
524
607
|
}
|
525
608
|
|
609
|
+
// Remove duplicates.
|
610
|
+
i = dups.length;
|
611
|
+
while (i--) {
|
612
|
+
models.splice(dups[i], 1);
|
613
|
+
}
|
614
|
+
|
526
615
|
// Listen to added models' events, and index models for lookup by
|
527
616
|
// `id` and by `cid`.
|
528
|
-
for (i = 0; i < length; i++) {
|
617
|
+
for (i = 0, length = models.length; i < length; i++) {
|
529
618
|
(model = models[i]).on('all', this._onModelEvent, this);
|
530
619
|
this._byCid[model.cid] = model;
|
531
620
|
if (model.id != null) this._byId[model.id] = model;
|
@@ -569,9 +658,37 @@
|
|
569
658
|
return this;
|
570
659
|
},
|
571
660
|
|
661
|
+
// Add a model to the end of the collection.
|
662
|
+
push: function(model, options) {
|
663
|
+
model = this._prepareModel(model, options);
|
664
|
+
this.add(model, options);
|
665
|
+
return model;
|
666
|
+
},
|
667
|
+
|
668
|
+
// Remove a model from the end of the collection.
|
669
|
+
pop: function(options) {
|
670
|
+
var model = this.at(this.length - 1);
|
671
|
+
this.remove(model, options);
|
672
|
+
return model;
|
673
|
+
},
|
674
|
+
|
675
|
+
// Add a model to the beginning of the collection.
|
676
|
+
unshift: function(model, options) {
|
677
|
+
model = this._prepareModel(model, options);
|
678
|
+
this.add(model, _.extend({at: 0}, options));
|
679
|
+
return model;
|
680
|
+
},
|
681
|
+
|
682
|
+
// Remove a model from the beginning of the collection.
|
683
|
+
shift: function(options) {
|
684
|
+
var model = this.at(0);
|
685
|
+
this.remove(model, options);
|
686
|
+
return model;
|
687
|
+
},
|
688
|
+
|
572
689
|
// Get a model from the set by id.
|
573
690
|
get: function(id) {
|
574
|
-
if (id == null) return
|
691
|
+
if (id == null) return void 0;
|
575
692
|
return this._byId[id.id != null ? id.id : id];
|
576
693
|
},
|
577
694
|
|
@@ -585,6 +702,17 @@
|
|
585
702
|
return this.models[index];
|
586
703
|
},
|
587
704
|
|
705
|
+
// Return models with matching attributes. Useful for simple cases of `filter`.
|
706
|
+
where: function(attrs) {
|
707
|
+
if (_.isEmpty(attrs)) return [];
|
708
|
+
return this.filter(function(model) {
|
709
|
+
for (var key in attrs) {
|
710
|
+
if (attrs[key] !== model.get(key)) return false;
|
711
|
+
}
|
712
|
+
return true;
|
713
|
+
});
|
714
|
+
},
|
715
|
+
|
588
716
|
// Force the collection to re-sort itself. You don't need to call this under
|
589
717
|
// normal circumstances, as the set will maintain sort order as each item
|
590
718
|
// is added.
|
@@ -616,7 +744,7 @@
|
|
616
744
|
this._removeReference(this.models[i]);
|
617
745
|
}
|
618
746
|
this._reset();
|
619
|
-
this.add(models, {silent: true,
|
747
|
+
this.add(models, _.extend({silent: true}, options));
|
620
748
|
if (!options.silent) this.trigger('reset', this, options);
|
621
749
|
return this;
|
622
750
|
},
|
@@ -682,7 +810,8 @@
|
|
682
810
|
|
683
811
|
// Prepare a model or hash of attributes to be added to this collection.
|
684
812
|
_prepareModel: function(model, options) {
|
685
|
-
|
813
|
+
options || (options = {});
|
814
|
+
if (!(model instanceof Model)) {
|
686
815
|
var attrs = model;
|
687
816
|
options.collection = this;
|
688
817
|
model = new this.model(attrs, options);
|
@@ -705,12 +834,12 @@
|
|
705
834
|
// Sets need to update their indexes when models change ids. All other
|
706
835
|
// events simply proxy through. "add" and "remove" events that originate
|
707
836
|
// in other collections are ignored.
|
708
|
-
_onModelEvent: function(
|
709
|
-
if ((
|
710
|
-
if (
|
837
|
+
_onModelEvent: function(event, model, collection, options) {
|
838
|
+
if ((event == 'add' || event == 'remove') && collection != this) return;
|
839
|
+
if (event == 'destroy') {
|
711
840
|
this.remove(model, options);
|
712
841
|
}
|
713
|
-
if (model &&
|
842
|
+
if (model && event === 'change:' + model.idAttribute) {
|
714
843
|
delete this._byId[model.previous(model.idAttribute)];
|
715
844
|
this._byId[model.id] = model;
|
716
845
|
}
|
@@ -728,7 +857,7 @@
|
|
728
857
|
|
729
858
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
730
859
|
_.each(methods, function(method) {
|
731
|
-
|
860
|
+
Collection.prototype[method] = function() {
|
732
861
|
return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
|
733
862
|
};
|
734
863
|
});
|
@@ -738,7 +867,7 @@
|
|
738
867
|
|
739
868
|
// Routers map faux-URLs to actions, and fire events when routes are
|
740
869
|
// matched. Creating a new one sets its `routes` hash, if not set statically.
|
741
|
-
Backbone.Router = function(options) {
|
870
|
+
var Router = Backbone.Router = function(options) {
|
742
871
|
options || (options = {});
|
743
872
|
if (options.routes) this.routes = options.routes;
|
744
873
|
this._bindRoutes();
|
@@ -752,7 +881,7 @@
|
|
752
881
|
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
|
753
882
|
|
754
883
|
// Set up all inheritable **Backbone.Router** properties and methods.
|
755
|
-
_.extend(
|
884
|
+
_.extend(Router.prototype, Events, {
|
756
885
|
|
757
886
|
// Initialize is an empty function by default. Override it with your own
|
758
887
|
// initialization logic.
|
@@ -765,7 +894,7 @@
|
|
765
894
|
// });
|
766
895
|
//
|
767
896
|
route: function(route, name, callback) {
|
768
|
-
Backbone.history || (Backbone.history = new
|
897
|
+
Backbone.history || (Backbone.history = new History);
|
769
898
|
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
770
899
|
if (!callback) callback = this[name];
|
771
900
|
Backbone.history.route(route, _.bind(function(fragment) {
|
@@ -818,7 +947,7 @@
|
|
818
947
|
|
819
948
|
// Handles cross-browser history management, based on URL fragments. If the
|
820
949
|
// browser does not support `onhashchange`, falls back to polling.
|
821
|
-
Backbone.History = function() {
|
950
|
+
var History = Backbone.History = function() {
|
822
951
|
this.handlers = [];
|
823
952
|
_.bindAll(this, 'checkUrl');
|
824
953
|
};
|
@@ -830,15 +959,23 @@
|
|
830
959
|
var isExplorer = /msie [\w.]+/;
|
831
960
|
|
832
961
|
// Has the history handling already been started?
|
833
|
-
|
962
|
+
History.started = false;
|
834
963
|
|
835
964
|
// Set up all inheritable **Backbone.History** properties and methods.
|
836
|
-
_.extend(
|
965
|
+
_.extend(History.prototype, Events, {
|
837
966
|
|
838
967
|
// The default interval to poll for hash changes, if necessary, is
|
839
968
|
// twenty times a second.
|
840
969
|
interval: 50,
|
841
970
|
|
971
|
+
// Gets the true hash value. Cannot use location.hash directly due to bug
|
972
|
+
// in Firefox where location.hash will always be decoded.
|
973
|
+
getHash: function(windowOverride) {
|
974
|
+
var loc = windowOverride ? windowOverride.location : window.location;
|
975
|
+
var match = loc.href.match(/#(.*)$/);
|
976
|
+
return match ? match[1] : '';
|
977
|
+
},
|
978
|
+
|
842
979
|
// Get the cross-browser normalized URL fragment, either from the URL,
|
843
980
|
// the hash, or the override.
|
844
981
|
getFragment: function(fragment, forcePushState) {
|
@@ -848,10 +985,9 @@
|
|
848
985
|
var search = window.location.search;
|
849
986
|
if (search) fragment += search;
|
850
987
|
} else {
|
851
|
-
fragment =
|
988
|
+
fragment = this.getHash();
|
852
989
|
}
|
853
990
|
}
|
854
|
-
fragment = decodeURIComponent(fragment);
|
855
991
|
if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
|
856
992
|
return fragment.replace(routeStripper, '');
|
857
993
|
},
|
@@ -859,10 +995,11 @@
|
|
859
995
|
// Start the hash change handling, returning `true` if the current URL matches
|
860
996
|
// an existing route, and `false` otherwise.
|
861
997
|
start: function(options) {
|
998
|
+
if (History.started) throw new Error("Backbone.history has already been started");
|
999
|
+
History.started = true;
|
862
1000
|
|
863
1001
|
// Figure out the initial configuration. Do we need an iframe?
|
864
1002
|
// Is pushState desired ... is it available?
|
865
|
-
if (historyStarted) throw new Error("Backbone.history has already been started");
|
866
1003
|
this.options = _.extend({}, {root: '/'}, this.options, options);
|
867
1004
|
this._wantsHashChange = this.options.hashChange !== false;
|
868
1005
|
this._wantsPushState = !!this.options.pushState;
|
@@ -870,6 +1007,7 @@
|
|
870
1007
|
var fragment = this.getFragment();
|
871
1008
|
var docMode = document.documentMode;
|
872
1009
|
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
|
1010
|
+
|
873
1011
|
if (oldIE) {
|
874
1012
|
this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
|
875
1013
|
this.navigate(fragment);
|
@@ -888,7 +1026,6 @@
|
|
888
1026
|
// Determine if we need to change the base url, for a pushState link
|
889
1027
|
// opened by a non-pushState browser.
|
890
1028
|
this.fragment = fragment;
|
891
|
-
historyStarted = true;
|
892
1029
|
var loc = window.location;
|
893
1030
|
var atRoot = loc.pathname == this.options.root;
|
894
1031
|
|
@@ -903,7 +1040,7 @@
|
|
903
1040
|
// Or if we've started out with a hash-based route, but we're currently
|
904
1041
|
// in a browser where it could be `pushState`-based instead...
|
905
1042
|
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
|
906
|
-
this.fragment =
|
1043
|
+
this.fragment = this.getHash().replace(routeStripper, '');
|
907
1044
|
window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
|
908
1045
|
}
|
909
1046
|
|
@@ -917,7 +1054,7 @@
|
|
917
1054
|
stop: function() {
|
918
1055
|
$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
|
919
1056
|
clearInterval(this._checkUrlInterval);
|
920
|
-
|
1057
|
+
History.started = false;
|
921
1058
|
},
|
922
1059
|
|
923
1060
|
// Add a route to be tested when the fragment changes. Routes added later
|
@@ -930,10 +1067,10 @@
|
|
930
1067
|
// calls `loadUrl`, normalizing across the hidden iframe.
|
931
1068
|
checkUrl: function(e) {
|
932
1069
|
var current = this.getFragment();
|
933
|
-
if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe
|
934
|
-
if (current == this.fragment
|
1070
|
+
if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
|
1071
|
+
if (current == this.fragment) return false;
|
935
1072
|
if (this.iframe) this.navigate(current);
|
936
|
-
this.loadUrl() || this.loadUrl(
|
1073
|
+
this.loadUrl() || this.loadUrl(this.getHash());
|
937
1074
|
},
|
938
1075
|
|
939
1076
|
// Attempt to load the current URL fragment. If a route succeeds with a
|
@@ -956,12 +1093,12 @@
|
|
956
1093
|
//
|
957
1094
|
// The options object can contain `trigger: true` if you wish to have the
|
958
1095
|
// route callback be fired (not usually desirable), or `replace: true`, if
|
959
|
-
// you
|
1096
|
+
// you wish to modify the current URL without adding an entry to the history.
|
960
1097
|
navigate: function(fragment, options) {
|
961
|
-
if (!
|
1098
|
+
if (!History.started) return false;
|
962
1099
|
if (!options || options === true) options = {trigger: options};
|
963
1100
|
var frag = (fragment || '').replace(routeStripper, '');
|
964
|
-
if (this.fragment == frag
|
1101
|
+
if (this.fragment == frag) return;
|
965
1102
|
|
966
1103
|
// If pushState is available, we use it to set the fragment as a real URL.
|
967
1104
|
if (this._hasPushState) {
|
@@ -974,7 +1111,7 @@
|
|
974
1111
|
} else if (this._wantsHashChange) {
|
975
1112
|
this.fragment = frag;
|
976
1113
|
this._updateHash(window.location, frag, options.replace);
|
977
|
-
if (this.iframe && (frag != this.getFragment(this.iframe
|
1114
|
+
if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
|
978
1115
|
// Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
|
979
1116
|
// When replace is true, we don't want this.
|
980
1117
|
if(!options.replace) this.iframe.document.open().close();
|
@@ -1005,7 +1142,7 @@
|
|
1005
1142
|
|
1006
1143
|
// Creating a Backbone.View creates its initial element outside of the DOM,
|
1007
1144
|
// if an existing element is not provided...
|
1008
|
-
Backbone.View = function(options) {
|
1145
|
+
var View = Backbone.View = function(options) {
|
1009
1146
|
this.cid = _.uniqueId('view');
|
1010
1147
|
this._configure(options || {});
|
1011
1148
|
this._ensureElement();
|
@@ -1014,13 +1151,13 @@
|
|
1014
1151
|
};
|
1015
1152
|
|
1016
1153
|
// Cached regex to split keys for `delegate`.
|
1017
|
-
var
|
1154
|
+
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
1018
1155
|
|
1019
1156
|
// List of view options to be merged as properties.
|
1020
1157
|
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
|
1021
1158
|
|
1022
1159
|
// Set up all inheritable **Backbone.View** properties and methods.
|
1023
|
-
_.extend(
|
1160
|
+
_.extend(View.prototype, Events, {
|
1024
1161
|
|
1025
1162
|
// The default `tagName` of a View's element is `"div"`.
|
1026
1163
|
tagName: 'div',
|
@@ -1064,7 +1201,8 @@
|
|
1064
1201
|
// Change the view's element (`this.el` property), including event
|
1065
1202
|
// re-delegation.
|
1066
1203
|
setElement: function(element, delegate) {
|
1067
|
-
this.$el
|
1204
|
+
if (this.$el) this.undelegateEvents();
|
1205
|
+
this.$el = (element instanceof $) ? element : $(element);
|
1068
1206
|
this.el = this.$el[0];
|
1069
1207
|
if (delegate !== false) this.delegateEvents();
|
1070
1208
|
return this;
|
@@ -1091,8 +1229,8 @@
|
|
1091
1229
|
for (var key in events) {
|
1092
1230
|
var method = events[key];
|
1093
1231
|
if (!_.isFunction(method)) method = this[events[key]];
|
1094
|
-
if (!method) throw new Error('
|
1095
|
-
var match = key.match(
|
1232
|
+
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
|
1233
|
+
var match = key.match(delegateEventSplitter);
|
1096
1234
|
var eventName = match[1], selector = match[2];
|
1097
1235
|
method = _.bind(method, this);
|
1098
1236
|
eventName += '.delegateEvents' + this.cid;
|
@@ -1148,8 +1286,7 @@
|
|
1148
1286
|
};
|
1149
1287
|
|
1150
1288
|
// Set up inheritance for the model, collection, and view.
|
1151
|
-
|
1152
|
-
Backbone.Router.extend = Backbone.View.extend = extend;
|
1289
|
+
Model.extend = Collection.extend = Router.extend = View.extend = extend;
|
1153
1290
|
|
1154
1291
|
// Backbone.sync
|
1155
1292
|
// -------------
|
@@ -1180,6 +1317,9 @@
|
|
1180
1317
|
Backbone.sync = function(method, model, options) {
|
1181
1318
|
var type = methodMap[method];
|
1182
1319
|
|
1320
|
+
// Default options, unless specified.
|
1321
|
+
options || (options = {});
|
1322
|
+
|
1183
1323
|
// Default JSON-request options.
|
1184
1324
|
var params = {type: type, dataType: 'json'};
|
1185
1325
|
|