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.
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