bone_tree 0.5.6 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.gitignore +26 -3
  2. data/Gemfile +5 -12
  3. data/Rakefile +7 -9
  4. data/bone_tree.gemspec +30 -14
  5. data/config.rb +8 -0
  6. data/lib/assets/javascripts/bone_tree.js +277 -981
  7. data/lib/assets/stylesheets/bone_tree.css +108 -0
  8. data/lib/bone_tree/rails.rb +5 -0
  9. data/lib/bone_tree/sprockets.rb +3 -0
  10. data/lib/{version.rb → bone_tree/version.rb} +1 -1
  11. data/lib/bone_tree.rb +11 -6
  12. data/source/index.html.haml +10 -382
  13. data/source/javascripts/{_backbone.js → backbone.js} +282 -142
  14. data/source/javascripts/bone_tree/models/directory.js.coffee +109 -0
  15. data/source/javascripts/bone_tree/models/file.js.coffee +12 -0
  16. data/source/javascripts/bone_tree/models/node.js.coffee +24 -0
  17. data/source/javascripts/bone_tree/models/settings.js.coffee +5 -0
  18. data/source/javascripts/bone_tree/{_namespace.js.coffee → namespace.js.coffee} +1 -1
  19. data/source/javascripts/bone_tree/views/directory.js.coffee +56 -0
  20. data/source/javascripts/bone_tree/views/file.js.coffee +25 -0
  21. data/source/javascripts/bone_tree/views/tree.js.coffee +87 -0
  22. data/source/javascripts/bone_tree.js.coffee +2 -1
  23. data/source/stylesheets/bone_tree.css.sass +0 -38
  24. data/spec/javascripts/directory_spec.coffee +26 -0
  25. data/spec/javascripts/helpers/spec_helper.coffee +3 -4
  26. data/spec/javascripts/sorting_spec.coffee +12 -0
  27. data/spec/javascripts/support/jasmine.yml +6 -4
  28. metadata +143 -36
  29. data/Gemfile.lock +0 -190
  30. data/docs/index.html +0 -222
  31. data/docs/resources/base.css +0 -70
  32. data/docs/resources/index.css +0 -20
  33. data/docs/resources/module.css +0 -24
  34. data/docs/source/javascripts/bone_tree/_namespace.js.html +0 -45
  35. data/docs/source/javascripts/bone_tree/models/_directory.js.html +0 -126
  36. data/docs/source/javascripts/bone_tree/models/_file.js.html +0 -112
  37. data/docs/source/javascripts/bone_tree/models/_nodes.js.html +0 -174
  38. data/docs/source/javascripts/bone_tree/models/_settings.js.html +0 -75
  39. data/docs/source/javascripts/bone_tree/views/_directory.js.html +0 -94
  40. data/docs/source/javascripts/bone_tree/views/_file.js.html +0 -82
  41. data/docs/source/javascripts/bone_tree/views/_menu.js.html +0 -110
  42. data/docs/source/javascripts/bone_tree/views/_tree.js.html +0 -432
  43. data/source/javascripts/_jquery.min.js +0 -5
  44. data/source/javascripts/_underscore.js +0 -999
  45. data/source/javascripts/bone_tree/models/_directory.js.coffee +0 -63
  46. data/source/javascripts/bone_tree/models/_file.js.coffee +0 -55
  47. data/source/javascripts/bone_tree/models/_nodes.js.coffee +0 -117
  48. data/source/javascripts/bone_tree/models/_settings.js.coffee +0 -25
  49. data/source/javascripts/bone_tree/views/_directory.js.coffee +0 -73
  50. data/source/javascripts/bone_tree/views/_file.js.coffee +0 -51
  51. data/source/javascripts/bone_tree/views/_menu.js.coffee +0 -97
  52. data/source/javascripts/bone_tree/views/_tree.js.coffee +0 -498
  53. data/spec/javascripts/directory_view_spec.coffee +0 -91
  54. data/spec/javascripts/file_view_spec.coffee +0 -70
  55. data/spec/javascripts/menu_view_spec.coffee +0 -42
  56. data/spec/javascripts/nodes_spec.coffee +0 -37
  57. data/spec/javascripts/tree_view_spec.coffee +0 -39
@@ -1,7 +1,6 @@
1
- //= require _jquery.min
2
- //= require _underscore
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.1';
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 an event, specified by a string name, `ev`, to a `callback`
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
- var ev;
92
- events = events.split(/\s+/);
93
- var calls = this._callbacks || (this._callbacks = {});
94
- while (ev = events.shift()) {
95
- // Create an immutable callback list, allowing traversal during
96
- // modification. The tail is an empty object that will always be used
97
- // as the next node.
98
- var list = calls[ev] || (calls[ev] = {});
99
- var tail = list.tail || (list.tail = list.next = {});
100
- tail.callback = callback;
101
- tail.context = context;
102
- list.tail = tail.next = {};
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 `ev` is null, removes all bound callbacks for all events.
116
+ // event. If `events` is null, removes all bound callbacks for all events.
110
117
  off: function(events, callback, context) {
111
- var ev, calls, node;
112
- if (!events) {
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
- } else if (calls = this._callbacks) {
115
- events = events.split(/\s+/);
116
- while (ev = events.shift()) {
117
- node = calls[ev];
118
- delete calls[ev];
119
- if (!callback || !node) continue;
120
- // Create a new list, omitting the indicated event/context pairs.
121
- while ((node = node.next) && node.next) {
122
- if (node.callback === callback &&
123
- (!context || node.context === context)) continue;
124
- this.on(ev, node.callback, node.context);
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 an event, firing all bound callbacks. Callbacks are passed the
132
- // same arguments as `trigger` is, apart from the event name.
133
- // Listening for `"all"` passes the true event name as the first argument.
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['all'];
138
- (events = events.split(/\s+/)).push(null);
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
- while (node = events.pop()) {
148
- tail = node.tail;
149
- args = node.event ? [node.event].concat(rest) : rest;
150
- while ((node = node.next) !== tail) {
151
- node.callback.apply(node.context || this, args);
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
- Backbone.Events.bind = Backbone.Events.on;
161
- Backbone.Events.unbind = Backbone.Events.off;
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
- if (!this.set(attributes, {silent: true})) {
180
- throw new Error("Can't create an invalid model");
181
- }
182
- delete this._changed;
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(Backbone.Model.prototype, Backbone.Events, {
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.attributes[attr];
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.attributes[attr] != null;
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 Backbone.Model) attrs = attrs.attributes;
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
- // Update attributes.
291
+ // For each `set` attribute...
254
292
  for (attr in attrs) {
255
293
  val = attrs[attr];
256
- if (!_.isEqual(now[attr], val)) delete escaped[attr];
257
- options.unset ? delete now[attr] : now[attr] = val;
258
- if (this._changing && !_.isEqual(this._changed[attr], val)) {
259
- this.trigger('change:' + attr, this, val, options);
260
- this._moreChanges = true;
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
- delete this._changed[attr];
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._changed[attr] = val;
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, if the model has been changed.
269
- if (!alreadySetting) {
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
- if (options.wait) current = _.clone(this.attributes);
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) serverAttrs = _.extend(attrs || {}, serverAttrs);
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()) return triggerDestroy();
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.collection, 'url') || getValue(this, 'urlRoot') || urlError();
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
- if (this._changing || !this.hasChanged()) return this;
464
+ options || (options = {});
465
+ var changing = this._changing;
400
466
  this._changing = true;
401
- this._moreChanges = true;
402
- for (var attr in this._changed) {
403
- this.trigger('change:' + attr, this, this._changed[attr], options);
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
- while (this._moreChanges) {
406
- this._moreChanges = false;
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
- this._previousAttributes = _.clone(this.attributes);
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._changed);
419
- return this._changed && _.has(this._changed, attr);
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._changed) : false;
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 a set of incoming attributes, returning `true`
458
- // if all is well. If a specific `error` callback has been passed,
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(Backbone.Collection.prototype, Backbone.Events, {
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: Backbone.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
- if (cids[cid = model.cid] || this._byCid[cid] ||
520
- (((id = model.id) != null) && (ids[id] || this._byId[id]))) {
521
- throw new Error("Can't add the same model to a collection twice");
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 null;
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, parse: options.parse});
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
- if (!(model instanceof Backbone.Model)) {
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(ev, model, collection, options) {
709
- if ((ev == 'add' || ev == 'remove') && collection != this) return;
710
- if (ev == 'destroy') {
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 && ev === 'change:' + model.idAttribute) {
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
- Backbone.Collection.prototype[method] = function() {
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(Backbone.Router.prototype, Backbone.Events, {
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 Backbone.History);
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
- var historyStarted = false;
962
+ History.started = false;
834
963
 
835
964
  // Set up all inheritable **Backbone.History** properties and methods.
836
- _.extend(Backbone.History.prototype, Backbone.Events, {
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 = window.location.hash;
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 = loc.hash.replace(routeStripper, '');
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
- historyStarted = false;
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.location.hash);
934
- if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
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(window.location.hash);
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 which to modify the current URL without adding an entry to the history.
1096
+ // you wish to modify the current URL without adding an entry to the history.
960
1097
  navigate: function(fragment, options) {
961
- if (!historyStarted) return false;
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 || this.fragment == decodeURIComponent(frag)) return;
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.location.hash))) {
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 eventSplitter = /^(\S+)\s*(.*)$/;
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(Backbone.View.prototype, Backbone.Events, {
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 = $(element);
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('Event "' + events[key] + '" does not exist');
1095
- var match = key.match(eventSplitter);
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
- Backbone.Model.extend = Backbone.Collection.extend =
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