rails-backbone 0.9.0 → 0.9.10

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Backbone-Rails [![Build Status](https://secure.travis-ci.org/codebrew/backbone-rails.png)](http://travis-ci.org/codebrew/backbone-rails)
2
2
 
3
- Easily setup and use backbone.js (0.9.9) with rails 3.1 and greater
3
+ Easily setup and use backbone.js (0.9.10) with Rails 3.1 and greater
4
4
 
5
5
  Follow [@TheRyanFitz on Twitter](http://twitter.com/#!/TheRyanFitz). Tweet any questions or suggestions you have about the project.
6
6
 
@@ -4,31 +4,31 @@ module Backbone
4
4
  module Generators
5
5
  class InstallGenerator < Rails::Generators::Base
6
6
  include Backbone::Generators::ResourceHelpers
7
-
7
+
8
8
  source_root File.expand_path("../templates", __FILE__)
9
-
9
+
10
10
  desc "This generator installs backbone.js with a default folder layout in app/assets/javascripts/backbone"
11
-
11
+
12
12
  class_option :skip_git, :type => :boolean, :aliases => "-G", :default => false,
13
13
  :desc => "Skip Git ignores and keeps"
14
-
14
+
15
15
  def inject_backbone
16
16
  inject_into_file "app/assets/javascripts/application.js", :before => "//= require_tree" do
17
17
  "//= require underscore\n//= require backbone\n//= require backbone_rails_sync\n//= require backbone_datalink\n//= require backbone/#{application_name.underscore}\n"
18
18
  end
19
19
  end
20
-
20
+
21
21
  def create_dir_layout
22
22
  %W{routers models views templates}.each do |dir|
23
23
  empty_directory "app/assets/javascripts/backbone/#{dir}"
24
24
  create_file "app/assets/javascripts/backbone/#{dir}/.gitkeep" unless options[:skip_git]
25
25
  end
26
26
  end
27
-
27
+
28
28
  def create_app_file
29
29
  template "app.coffee", "app/assets/javascripts/backbone/#{application_name.underscore}.js.coffee"
30
30
  end
31
-
31
+
32
32
  end
33
33
  end
34
- end
34
+ end
@@ -1,4 +1,4 @@
1
- // Backbone.js 0.9.9
1
+ // Backbone.js 0.9.10
2
2
 
3
3
  // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
4
4
  // Backbone may be freely distributed under the MIT license.
@@ -34,7 +34,7 @@
34
34
  }
35
35
 
36
36
  // Current version of the library. Keep in sync with `package.json`.
37
- Backbone.VERSION = '0.9.9';
37
+ Backbone.VERSION = '0.9.10';
38
38
 
39
39
  // Require Underscore, if we're on the server, and it's not already present.
40
40
  var _ = root._;
@@ -88,7 +88,7 @@
88
88
 
89
89
  // Optimized internal dispatch function for triggering events. Tries to
90
90
  // keep the usual cases speedy (most Backbone events have 3 arguments).
91
- var triggerEvents = function(obj, events, args) {
91
+ var triggerEvents = function(events, args) {
92
92
  var ev, i = -1, l = events.length;
93
93
  switch (args.length) {
94
94
  case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
@@ -142,7 +142,7 @@
142
142
 
143
143
  // Remove one or many callbacks. If `context` is null, removes all
144
144
  // callbacks with that function. If `callback` is null, removes all
145
- // callbacks for the event. If `events` is null, removes all bound
145
+ // callbacks for the event. If `name` is null, removes all bound
146
146
  // callbacks for all events.
147
147
  off: function(name, callback, context) {
148
148
  var list, ev, events, names, i, l, j, k;
@@ -160,7 +160,8 @@
160
160
  if (callback || context) {
161
161
  for (j = 0, k = list.length; j < k; j++) {
162
162
  ev = list[j];
163
- if ((callback && callback !== (ev.callback._callback || ev.callback)) ||
163
+ if ((callback && callback !== ev.callback &&
164
+ callback !== ev.callback._callback) ||
164
165
  (context && context !== ev.context)) {
165
166
  events.push(ev);
166
167
  }
@@ -183,32 +184,33 @@
183
184
  if (!eventsApi(this, 'trigger', name, args)) return this;
184
185
  var events = this._events[name];
185
186
  var allEvents = this._events.all;
186
- if (events) triggerEvents(this, events, args);
187
- if (allEvents) triggerEvents(this, allEvents, arguments);
187
+ if (events) triggerEvents(events, args);
188
+ if (allEvents) triggerEvents(allEvents, arguments);
188
189
  return this;
189
190
  },
190
191
 
191
192
  // An inversion-of-control version of `on`. Tell *this* object to listen to
192
193
  // an event in another object ... keeping track of what it's listening to.
193
- listenTo: function(object, events, callback) {
194
+ listenTo: function(obj, name, callback) {
194
195
  var listeners = this._listeners || (this._listeners = {});
195
- var id = object._listenerId || (object._listenerId = _.uniqueId('l'));
196
- listeners[id] = object;
197
- object.on(events, callback || this, this);
196
+ var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
197
+ listeners[id] = obj;
198
+ obj.on(name, typeof name === 'object' ? this : callback, this);
198
199
  return this;
199
200
  },
200
201
 
201
202
  // Tell this object to stop listening to either specific events ... or
202
203
  // to every object it's currently listening to.
203
- stopListening: function(object, events, callback) {
204
+ stopListening: function(obj, name, callback) {
204
205
  var listeners = this._listeners;
205
206
  if (!listeners) return;
206
- if (object) {
207
- object.off(events, callback, this);
208
- if (!events && !callback) delete listeners[object._listenerId];
207
+ if (obj) {
208
+ obj.off(name, typeof name === 'object' ? this : callback, this);
209
+ if (!name && !callback) delete listeners[obj._listenerId];
209
210
  } else {
211
+ if (typeof name === 'object') callback = this;
210
212
  for (var id in listeners) {
211
- listeners[id].off(null, null, this);
213
+ listeners[id].off(name, callback, this);
212
214
  }
213
215
  this._listeners = {};
214
216
  }
@@ -233,15 +235,14 @@
233
235
  var defaults;
234
236
  var attrs = attributes || {};
235
237
  this.cid = _.uniqueId('c');
236
- this.changed = {};
237
238
  this.attributes = {};
238
- this._changes = [];
239
239
  if (options && options.collection) this.collection = options.collection;
240
- if (options && options.parse) attrs = this.parse(attrs);
241
- if (defaults = _.result(this, 'defaults')) _.defaults(attrs, defaults);
242
- this.set(attrs, {silent: true});
243
- this._currentAttributes = _.clone(this.attributes);
244
- this._previousAttributes = _.clone(this.attributes);
240
+ if (options && options.parse) attrs = this.parse(attrs, options) || {};
241
+ if (defaults = _.result(this, 'defaults')) {
242
+ attrs = _.defaults({}, attrs, defaults);
243
+ }
244
+ this.set(attrs, options);
245
+ this.changed = {};
245
246
  this.initialize.apply(this, arguments);
246
247
  };
247
248
 
@@ -285,47 +286,72 @@
285
286
  return this.get(attr) != null;
286
287
  },
287
288
 
289
+ // ----------------------------------------------------------------------
290
+
288
291
  // Set a hash of model attributes on the object, firing `"change"` unless
289
292
  // you choose to silence it.
290
293
  set: function(key, val, options) {
291
- var attr, attrs;
294
+ var attr, attrs, unset, changes, silent, changing, prev, current;
292
295
  if (key == null) return this;
293
296
 
294
297
  // Handle both `"key", value` and `{key: value}` -style arguments.
295
- if (_.isObject(key)) {
298
+ if (typeof key === 'object') {
296
299
  attrs = key;
297
300
  options = val;
298
301
  } else {
299
302
  (attrs = {})[key] = val;
300
303
  }
301
304
 
302
- // Extract attributes and options.
303
- var silent = options && options.silent;
304
- var unset = options && options.unset;
305
+ options || (options = {});
305
306
 
306
307
  // Run validation.
307
308
  if (!this._validate(attrs, options)) return false;
308
309
 
310
+ // Extract attributes and options.
311
+ unset = options.unset;
312
+ silent = options.silent;
313
+ changes = [];
314
+ changing = this._changing;
315
+ this._changing = true;
316
+
317
+ if (!changing) {
318
+ this._previousAttributes = _.clone(this.attributes);
319
+ this.changed = {};
320
+ }
321
+ current = this.attributes, prev = this._previousAttributes;
322
+
309
323
  // Check for changes of `id`.
310
324
  if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
311
325
 
312
- var now = this.attributes;
313
-
314
- // For each `set` attribute...
326
+ // For each `set` attribute, update or delete the current value.
315
327
  for (attr in attrs) {
316
328
  val = attrs[attr];
317
-
318
- // Update or delete the current value, and track the change.
319
- unset ? delete now[attr] : now[attr] = val;
320
- this._changes.push(attr, val);
329
+ if (!_.isEqual(current[attr], val)) changes.push(attr);
330
+ if (!_.isEqual(prev[attr], val)) {
331
+ this.changed[attr] = val;
332
+ } else {
333
+ delete this.changed[attr];
334
+ }
335
+ unset ? delete current[attr] : current[attr] = val;
321
336
  }
322
337
 
323
- // Signal that the model's state has potentially changed, and we need
324
- // to recompute the actual changes.
325
- this._hasComputed = false;
338
+ // Trigger all relevant attribute changes.
339
+ if (!silent) {
340
+ if (changes.length) this._pending = true;
341
+ for (var i = 0, l = changes.length; i < l; i++) {
342
+ this.trigger('change:' + changes[i], this, current[changes[i]], options);
343
+ }
344
+ }
326
345
 
327
- // Fire the `"change"` events.
328
- if (!silent) this.change(options);
346
+ if (changing) return this;
347
+ if (!silent) {
348
+ while (this._pending) {
349
+ this._pending = false;
350
+ this.trigger('change', this, options);
351
+ }
352
+ }
353
+ this._pending = false;
354
+ this._changing = false;
329
355
  return this;
330
356
  },
331
357
 
@@ -343,16 +369,54 @@
343
369
  return this.set(attrs, _.extend({}, options, {unset: true}));
344
370
  },
345
371
 
372
+ // Determine if the model has changed since the last `"change"` event.
373
+ // If you specify an attribute name, determine if that attribute has changed.
374
+ hasChanged: function(attr) {
375
+ if (attr == null) return !_.isEmpty(this.changed);
376
+ return _.has(this.changed, attr);
377
+ },
378
+
379
+ // Return an object containing all the attributes that have changed, or
380
+ // false if there are no changed attributes. Useful for determining what
381
+ // parts of a view need to be updated and/or what attributes need to be
382
+ // persisted to the server. Unset attributes will be set to undefined.
383
+ // You can also pass an attributes object to diff against the model,
384
+ // determining if there *would be* a change.
385
+ changedAttributes: function(diff) {
386
+ if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
387
+ var val, changed = false;
388
+ var old = this._changing ? this._previousAttributes : this.attributes;
389
+ for (var attr in diff) {
390
+ if (_.isEqual(old[attr], (val = diff[attr]))) continue;
391
+ (changed || (changed = {}))[attr] = val;
392
+ }
393
+ return changed;
394
+ },
395
+
396
+ // Get the previous value of an attribute, recorded at the time the last
397
+ // `"change"` event was fired.
398
+ previous: function(attr) {
399
+ if (attr == null || !this._previousAttributes) return null;
400
+ return this._previousAttributes[attr];
401
+ },
402
+
403
+ // Get all of the attributes of the model at the time of the previous
404
+ // `"change"` event.
405
+ previousAttributes: function() {
406
+ return _.clone(this._previousAttributes);
407
+ },
408
+
409
+ // ---------------------------------------------------------------------
410
+
346
411
  // Fetch the model from the server. If the server's representation of the
347
412
  // model differs from its current attributes, they will be overriden,
348
413
  // triggering a `"change"` event.
349
414
  fetch: function(options) {
350
415
  options = options ? _.clone(options) : {};
351
416
  if (options.parse === void 0) options.parse = true;
352
- var model = this;
353
417
  var success = options.success;
354
- options.success = function(resp, status, xhr) {
355
- if (!model.set(model.parse(resp), options)) return false;
418
+ options.success = function(model, resp, options) {
419
+ if (!model.set(model.parse(resp, options), options)) return false;
356
420
  if (success) success(model, resp, options);
357
421
  };
358
422
  return this.sync('read', this, options);
@@ -362,55 +426,51 @@
362
426
  // If the server returns an attributes hash that differs, the model's
363
427
  // state will be `set` again.
364
428
  save: function(key, val, options) {
365
- var attrs, current, done;
429
+ var attrs, success, method, xhr, attributes = this.attributes;
366
430
 
367
431
  // Handle both `"key", value` and `{key: value}` -style arguments.
368
- if (key == null || _.isObject(key)) {
432
+ if (key == null || typeof key === 'object') {
369
433
  attrs = key;
370
434
  options = val;
371
- } else if (key != null) {
435
+ } else {
372
436
  (attrs = {})[key] = val;
373
437
  }
374
- options = options ? _.clone(options) : {};
375
438
 
376
- // If we're "wait"-ing to set changed attributes, validate early.
377
- if (options.wait) {
378
- if (attrs && !this._validate(attrs, options)) return false;
379
- current = _.clone(this.attributes);
380
- }
439
+ // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
440
+ if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
381
441
 
382
- // Regular saves `set` attributes before persisting to the server.
383
- var silentOptions = _.extend({}, options, {silent: true});
384
- if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
385
- return false;
386
- }
442
+ options = _.extend({validate: true}, options);
387
443
 
388
444
  // Do not persist invalid models.
389
- if (!attrs && !this._validate(null, options)) return false;
445
+ if (!this._validate(attrs, options)) return false;
446
+
447
+ // Set temporary attributes if `{wait: true}`.
448
+ if (attrs && options.wait) {
449
+ this.attributes = _.extend({}, attributes, attrs);
450
+ }
390
451
 
391
452
  // After a successful server-side save, the client is (optionally)
392
453
  // updated with the server-side state.
393
- var model = this;
394
- var success = options.success;
395
- options.success = function(resp, status, xhr) {
396
- done = true;
397
- var serverAttrs = model.parse(resp);
454
+ if (options.parse === void 0) options.parse = true;
455
+ success = options.success;
456
+ options.success = function(model, resp, options) {
457
+ // Ensure attributes are restored during synchronous saves.
458
+ model.attributes = attributes;
459
+ var serverAttrs = model.parse(resp, options);
398
460
  if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
399
- if (!model.set(serverAttrs, options)) return false;
461
+ if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
462
+ return false;
463
+ }
400
464
  if (success) success(model, resp, options);
401
465
  };
402
466
 
403
467
  // Finish configuring and sending the Ajax request.
404
- var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
405
- if (method == 'patch') options.attrs = attrs;
406
- var xhr = this.sync(method, this, options);
407
-
408
- // When using `wait`, reset attributes to original values unless
409
- // `success` has been called already.
410
- if (!done && options.wait) {
411
- this.clear(silentOptions);
412
- this.set(current, silentOptions);
413
- }
468
+ method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
469
+ if (method === 'patch') options.attrs = attrs;
470
+ xhr = this.sync(method, this, options);
471
+
472
+ // Restore attributes.
473
+ if (attrs && options.wait) this.attributes = attributes;
414
474
 
415
475
  return xhr;
416
476
  },
@@ -427,13 +487,13 @@
427
487
  model.trigger('destroy', model, model.collection, options);
428
488
  };
429
489
 
430
- options.success = function(resp) {
490
+ options.success = function(model, resp, options) {
431
491
  if (options.wait || model.isNew()) destroy();
432
492
  if (success) success(model, resp, options);
433
493
  };
434
494
 
435
495
  if (this.isNew()) {
436
- options.success();
496
+ options.success(this, null, options);
437
497
  return false;
438
498
  }
439
499
 
@@ -453,7 +513,7 @@
453
513
 
454
514
  // **parse** converts a response into the hash of attributes to be `set` on
455
515
  // the model. The default implementation is just to pass the response along.
456
- parse: function(resp) {
516
+ parse: function(resp, options) {
457
517
  return resp;
458
518
  },
459
519
 
@@ -467,115 +527,20 @@
467
527
  return this.id == null;
468
528
  },
469
529
 
470
- // Call this method to manually fire a `"change"` event for this model and
471
- // a `"change:attribute"` event for each changed attribute.
472
- // Calling this will cause all objects observing the model to update.
473
- change: function(options) {
474
- var changing = this._changing;
475
- this._changing = true;
476
-
477
- // Generate the changes to be triggered on the model.
478
- var triggers = this._computeChanges(true);
479
-
480
- this._pending = !!triggers.length;
481
-
482
- for (var i = triggers.length - 2; i >= 0; i -= 2) {
483
- this.trigger('change:' + triggers[i], this, triggers[i + 1], options);
484
- }
485
-
486
- if (changing) return this;
487
-
488
- // Trigger a `change` while there have been changes.
489
- while (this._pending) {
490
- this._pending = false;
491
- this.trigger('change', this, options);
492
- this._previousAttributes = _.clone(this.attributes);
493
- }
494
-
495
- this._changing = false;
496
- return this;
497
- },
498
-
499
- // Determine if the model has changed since the last `"change"` event.
500
- // If you specify an attribute name, determine if that attribute has changed.
501
- hasChanged: function(attr) {
502
- if (!this._hasComputed) this._computeChanges();
503
- if (attr == null) return !_.isEmpty(this.changed);
504
- return _.has(this.changed, attr);
505
- },
506
-
507
- // Return an object containing all the attributes that have changed, or
508
- // false if there are no changed attributes. Useful for determining what
509
- // parts of a view need to be updated and/or what attributes need to be
510
- // persisted to the server. Unset attributes will be set to undefined.
511
- // You can also pass an attributes object to diff against the model,
512
- // determining if there *would be* a change.
513
- changedAttributes: function(diff) {
514
- if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
515
- var val, changed = false, old = this._previousAttributes;
516
- for (var attr in diff) {
517
- if (_.isEqual(old[attr], (val = diff[attr]))) continue;
518
- (changed || (changed = {}))[attr] = val;
519
- }
520
- return changed;
521
- },
522
-
523
- // Looking at the built up list of `set` attribute changes, compute how
524
- // many of the attributes have actually changed. If `loud`, return a
525
- // boiled-down list of only the real changes.
526
- _computeChanges: function(loud) {
527
- this.changed = {};
528
- var already = {};
529
- var triggers = [];
530
- var current = this._currentAttributes;
531
- var changes = this._changes;
532
-
533
- // Loop through the current queue of potential model changes.
534
- for (var i = changes.length - 2; i >= 0; i -= 2) {
535
- var key = changes[i], val = changes[i + 1];
536
- if (already[key]) continue;
537
- already[key] = true;
538
-
539
- // Check if the attribute has been modified since the last change,
540
- // and update `this.changed` accordingly. If we're inside of a `change`
541
- // call, also add a trigger to the list.
542
- if (current[key] !== val) {
543
- this.changed[key] = val;
544
- if (!loud) continue;
545
- triggers.push(key, val);
546
- current[key] = val;
547
- }
548
- }
549
- if (loud) this._changes = [];
550
-
551
- // Signals `this.changed` is current to prevent duplicate calls from `this.hasChanged`.
552
- this._hasComputed = true;
553
- return triggers;
554
- },
555
-
556
- // Get the previous value of an attribute, recorded at the time the last
557
- // `"change"` event was fired.
558
- previous: function(attr) {
559
- if (attr == null || !this._previousAttributes) return null;
560
- return this._previousAttributes[attr];
561
- },
562
-
563
- // Get all of the attributes of the model at the time of the previous
564
- // `"change"` event.
565
- previousAttributes: function() {
566
- return _.clone(this._previousAttributes);
530
+ // Check if the model is currently in a valid state.
531
+ isValid: function(options) {
532
+ return !this.validate || !this.validate(this.attributes, options);
567
533
  },
568
534
 
569
535
  // Run validation against the next complete set of model attributes,
570
- // returning `true` if all is well. If a specific `error` callback has
571
- // been passed, call that instead of firing the general `"error"` event.
536
+ // returning `true` if all is well. Otherwise, fire a general
537
+ // `"error"` event and call the error callback, if specified.
572
538
  _validate: function(attrs, options) {
573
- if (!this.validate) return true;
539
+ if (!options.validate || !this.validate) return true;
574
540
  attrs = _.extend({}, this.attributes, attrs);
575
- var error = this.validate(attrs, options);
541
+ var error = this.validationError = this.validate(attrs, options) || null;
576
542
  if (!error) return true;
577
- if (options && options.error) options.error(this, error, options);
578
- this.trigger('error', this, error, options);
543
+ this.trigger('invalid', this, error, options || {});
579
544
  return false;
580
545
  }
581
546
 
@@ -591,6 +556,7 @@
591
556
  options || (options = {});
592
557
  if (options.model) this.model = options.model;
593
558
  if (options.comparator !== void 0) this.comparator = options.comparator;
559
+ this.models = [];
594
560
  this._reset();
595
561
  this.initialize.apply(this, arguments);
596
562
  if (models) this.reset(models, _.extend({silent: true}, options));
@@ -618,74 +584,81 @@
618
584
  return Backbone.sync.apply(this, arguments);
619
585
  },
620
586
 
621
- // Add a model, or list of models to the set. Pass **silent** to avoid
622
- // firing the `add` event for every new model.
587
+ // Add a model, or list of models to the set.
623
588
  add: function(models, options) {
624
- var i, args, length, model, existing, needsSort;
625
- var at = options && options.at;
626
- var sort = ((options && options.sort) == null ? true : options.sort);
627
589
  models = _.isArray(models) ? models.slice() : [models];
590
+ options || (options = {});
591
+ var i, l, model, attrs, existing, doSort, add, at, sort, sortAttr;
592
+ add = [];
593
+ at = options.at;
594
+ sort = this.comparator && (at == null) && options.sort != false;
595
+ sortAttr = _.isString(this.comparator) ? this.comparator : null;
628
596
 
629
597
  // Turn bare objects into model references, and prevent invalid models
630
598
  // from being added.
631
- for (i = models.length - 1; i >= 0; i--) {
632
- if(!(model = this._prepareModel(models[i], options))) {
633
- this.trigger("error", this, models[i], options);
634
- models.splice(i, 1);
599
+ for (i = 0, l = models.length; i < l; i++) {
600
+ if (!(model = this._prepareModel(attrs = models[i], options))) {
601
+ this.trigger('invalid', this, attrs, options);
635
602
  continue;
636
603
  }
637
- models[i] = model;
638
604
 
639
- existing = model.id != null && this._byId[model.id];
640
605
  // If a duplicate is found, prevent it from being added and
641
606
  // optionally merge it into the existing model.
642
- if (existing || this._byCid[model.cid]) {
643
- if (options && options.merge && existing) {
644
- existing.set(model.attributes, options);
645
- needsSort = sort;
607
+ if (existing = this.get(model)) {
608
+ if (options.merge) {
609
+ existing.set(attrs === model ? model.attributes : attrs, options);
610
+ if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true;
646
611
  }
647
- models.splice(i, 1);
648
612
  continue;
649
613
  }
650
614
 
615
+ // This is a new model, push it to the `add` list.
616
+ add.push(model);
617
+
651
618
  // Listen to added models' events, and index models for lookup by
652
619
  // `id` and by `cid`.
653
620
  model.on('all', this._onModelEvent, this);
654
- this._byCid[model.cid] = model;
621
+ this._byId[model.cid] = model;
655
622
  if (model.id != null) this._byId[model.id] = model;
656
623
  }
657
624
 
658
625
  // See if sorting is needed, update `length` and splice in new models.
659
- if (models.length) needsSort = sort;
660
- this.length += models.length;
661
- args = [at != null ? at : this.models.length, 0];
662
- push.apply(args, models);
663
- splice.apply(this.models, args);
626
+ if (add.length) {
627
+ if (sort) doSort = true;
628
+ this.length += add.length;
629
+ if (at != null) {
630
+ splice.apply(this.models, [at, 0].concat(add));
631
+ } else {
632
+ push.apply(this.models, add);
633
+ }
634
+ }
664
635
 
665
- // Sort the collection if appropriate.
666
- if (needsSort && this.comparator && at == null) this.sort({silent: true});
636
+ // Silently sort the collection if appropriate.
637
+ if (doSort) this.sort({silent: true});
667
638
 
668
- if (options && options.silent) return this;
639
+ if (options.silent) return this;
669
640
 
670
641
  // Trigger `add` events.
671
- while (model = models.shift()) {
672
- model.trigger('add', model, this, options);
642
+ for (i = 0, l = add.length; i < l; i++) {
643
+ (model = add[i]).trigger('add', model, this, options);
673
644
  }
674
645
 
646
+ // Trigger `sort` if the collection was sorted.
647
+ if (doSort) this.trigger('sort', this, options);
648
+
675
649
  return this;
676
650
  },
677
651
 
678
- // Remove a model, or a list of models from the set. Pass silent to avoid
679
- // firing the `remove` event for every model removed.
652
+ // Remove a model, or a list of models from the set.
680
653
  remove: function(models, options) {
681
- var i, l, index, model;
682
- options || (options = {});
683
654
  models = _.isArray(models) ? models.slice() : [models];
655
+ options || (options = {});
656
+ var i, l, index, model;
684
657
  for (i = 0, l = models.length; i < l; i++) {
685
658
  model = this.get(models[i]);
686
659
  if (!model) continue;
687
660
  delete this._byId[model.id];
688
- delete this._byCid[model.cid];
661
+ delete this._byId[model.cid];
689
662
  index = this.indexOf(model);
690
663
  this.models.splice(index, 1);
691
664
  this.length--;
@@ -734,7 +707,8 @@
734
707
  // Get a model from the set by id.
735
708
  get: function(obj) {
736
709
  if (obj == null) return void 0;
737
- return this._byId[obj.id != null ? obj.id : obj] || this._byCid[obj.cid || obj];
710
+ this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
711
+ return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
738
712
  },
739
713
 
740
714
  // Get the model at the given index.
@@ -760,14 +734,16 @@
760
734
  if (!this.comparator) {
761
735
  throw new Error('Cannot sort a set without a comparator');
762
736
  }
737
+ options || (options = {});
763
738
 
739
+ // Run sort based on type of `comparator`.
764
740
  if (_.isString(this.comparator) || this.comparator.length === 1) {
765
741
  this.models = this.sortBy(this.comparator, this);
766
742
  } else {
767
743
  this.models.sort(_.bind(this.comparator, this));
768
744
  }
769
745
 
770
- if (!options || !options.silent) this.trigger('sort', this, options);
746
+ if (!options.silent) this.trigger('sort', this, options);
771
747
  return this;
772
748
  },
773
749
 
@@ -779,11 +755,10 @@
779
755
  // Smartly update a collection with a change set of models, adding,
780
756
  // removing, and merging as necessary.
781
757
  update: function(models, options) {
758
+ options = _.extend({add: true, merge: true, remove: true}, options);
759
+ if (options.parse) models = this.parse(models, options);
782
760
  var model, i, l, existing;
783
761
  var add = [], remove = [], modelMap = {};
784
- var idAttr = this.model.prototype.idAttribute;
785
- options = _.extend({add: true, merge: true, remove: true}, options);
786
- if (options.parse) models = this.parse(models);
787
762
 
788
763
  // Allow a single model (or no argument) to be passed.
789
764
  if (!_.isArray(models)) models = models ? [models] : [];
@@ -794,7 +769,7 @@
794
769
  // Determine which models to add and merge, and which to remove.
795
770
  for (i = 0, l = models.length; i < l; i++) {
796
771
  model = models[i];
797
- existing = this.get(model.id || model.cid || model[idAttr]);
772
+ existing = this.get(model);
798
773
  if (options.remove && existing) modelMap[existing.cid] = true;
799
774
  if ((options.add && !existing) || (options.merge && existing)) {
800
775
  add.push(model);
@@ -818,11 +793,11 @@
818
793
  // any `add` or `remove` events. Fires `reset` when finished.
819
794
  reset: function(models, options) {
820
795
  options || (options = {});
821
- if (options.parse) models = this.parse(models);
796
+ if (options.parse) models = this.parse(models, options);
822
797
  for (var i = 0, l = this.models.length; i < l; i++) {
823
798
  this._removeReference(this.models[i]);
824
799
  }
825
- options.previousModels = this.models;
800
+ options.previousModels = this.models.slice();
826
801
  this._reset();
827
802
  if (models) this.add(models, _.extend({silent: true}, options));
828
803
  if (!options.silent) this.trigger('reset', this, options);
@@ -830,14 +805,13 @@
830
805
  },
831
806
 
832
807
  // Fetch the default set of models for this collection, resetting the
833
- // collection when they arrive. If `add: true` is passed, appends the
834
- // models to the collection instead of resetting.
808
+ // collection when they arrive. If `update: true` is passed, the response
809
+ // data will be passed through the `update` method instead of `reset`.
835
810
  fetch: function(options) {
836
811
  options = options ? _.clone(options) : {};
837
812
  if (options.parse === void 0) options.parse = true;
838
- var collection = this;
839
813
  var success = options.success;
840
- options.success = function(resp, status, xhr) {
814
+ options.success = function(collection, resp, options) {
841
815
  var method = options.update ? 'update' : 'reset';
842
816
  collection[method](resp, options);
843
817
  if (success) success(collection, resp, options);
@@ -849,11 +823,10 @@
849
823
  // collection immediately, unless `wait: true` is passed, in which case we
850
824
  // wait for the server to agree.
851
825
  create: function(model, options) {
852
- var collection = this;
853
826
  options = options ? _.clone(options) : {};
854
- model = this._prepareModel(model, options);
855
- if (!model) return false;
856
- if (!options.wait) collection.add(model, options);
827
+ if (!(model = this._prepareModel(model, options))) return false;
828
+ if (!options.wait) this.add(model, options);
829
+ var collection = this;
857
830
  var success = options.success;
858
831
  options.success = function(model, resp, options) {
859
832
  if (options.wait) collection.add(model, options);
@@ -865,7 +838,7 @@
865
838
 
866
839
  // **parse** converts a response into a list of models to be added to the
867
840
  // collection. The default implementation is just to pass it through.
868
- parse: function(resp) {
841
+ parse: function(resp, options) {
869
842
  return resp;
870
843
  },
871
844
 
@@ -874,19 +847,11 @@
874
847
  return new this.constructor(this.models);
875
848
  },
876
849
 
877
- // Proxy to _'s chain. Can't be proxied the same way the rest of the
878
- // underscore methods are proxied because it relies on the underscore
879
- // constructor.
880
- chain: function() {
881
- return _(this.models).chain();
882
- },
883
-
884
850
  // Reset all internal state. Called when the collection is reset.
885
851
  _reset: function() {
886
852
  this.length = 0;
887
- this.models = [];
853
+ this.models.length = 0;
888
854
  this._byId = {};
889
- this._byCid = {};
890
855
  },
891
856
 
892
857
  // Prepare a model or hash of attributes to be added to this collection.
@@ -920,6 +885,14 @@
920
885
  if (model.id != null) this._byId[model.id] = model;
921
886
  }
922
887
  this.trigger.apply(this, arguments);
888
+ },
889
+
890
+ sortedIndex: function (model, value, context) {
891
+ value || (value = this.comparator);
892
+ var iterator = _.isFunction(value) ? value : function(model) {
893
+ return model.get(value);
894
+ };
895
+ return _.sortedIndex(this.models, model, iterator, context);
923
896
  }
924
897
 
925
898
  });
@@ -928,9 +901,9 @@
928
901
  var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
929
902
  'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
930
903
  'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
931
- 'max', 'min', 'sortedIndex', 'toArray', 'size', 'first', 'head', 'take',
932
- 'initial', 'rest', 'tail', 'last', 'without', 'indexOf', 'shuffle',
933
- 'lastIndexOf', 'isEmpty'];
904
+ 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
905
+ 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
906
+ 'isEmpty', 'chain'];
934
907
 
935
908
  // Mix in each Underscore method as a proxy to `Collection#models`.
936
909
  _.each(methods, function(method) {
@@ -969,7 +942,7 @@
969
942
  // Cached regular expressions for matching named param parts and splatted
970
943
  // parts of route strings.
971
944
  var optionalParam = /\((.*?)\)/g;
972
- var namedParam = /:\w+/g;
945
+ var namedParam = /(\(\?)?:\w+/g;
973
946
  var splatParam = /\*\w+/g;
974
947
  var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
975
948
 
@@ -993,6 +966,7 @@
993
966
  var args = this._extractParameters(route, fragment);
994
967
  callback && callback.apply(this, args);
995
968
  this.trigger.apply(this, ['route:' + name].concat(args));
969
+ this.trigger('route', name, args);
996
970
  Backbone.history.trigger('route', this, name, args);
997
971
  }, this));
998
972
  return this;
@@ -1020,7 +994,9 @@
1020
994
  _routeToRegExp: function(route) {
1021
995
  route = route.replace(escapeRegExp, '\\$&')
1022
996
  .replace(optionalParam, '(?:$1)?')
1023
- .replace(namedParam, '([^\/]+)')
997
+ .replace(namedParam, function(match, optional){
998
+ return optional ? match : '([^\/]+)';
999
+ })
1024
1000
  .replace(splatParam, '(.*?)');
1025
1001
  return new RegExp('^' + route + '$');
1026
1002
  },
@@ -1042,7 +1018,7 @@
1042
1018
  this.handlers = [];
1043
1019
  _.bindAll(this, 'checkUrl');
1044
1020
 
1045
- // #1653 - Ensure that `History` can be used outside of the browser.
1021
+ // Ensure that `History` can be used outside of the browser.
1046
1022
  if (typeof window !== 'undefined') {
1047
1023
  this.location = window.location;
1048
1024
  this.history = window.history;
@@ -1121,9 +1097,9 @@
1121
1097
  // Depending on whether we're using pushState or hashes, and whether
1122
1098
  // 'onhashchange' is supported, determine how we check the URL state.
1123
1099
  if (this._hasPushState) {
1124
- Backbone.$(window).bind('popstate', this.checkUrl);
1100
+ Backbone.$(window).on('popstate', this.checkUrl);
1125
1101
  } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1126
- Backbone.$(window).bind('hashchange', this.checkUrl);
1102
+ Backbone.$(window).on('hashchange', this.checkUrl);
1127
1103
  } else if (this._wantsHashChange) {
1128
1104
  this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1129
1105
  }
@@ -1155,7 +1131,7 @@
1155
1131
  // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1156
1132
  // but possibly useful for unit testing Routers.
1157
1133
  stop: function() {
1158
- Backbone.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
1134
+ Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1159
1135
  clearInterval(this._checkUrlInterval);
1160
1136
  History.started = false;
1161
1137
  },
@@ -1238,7 +1214,7 @@
1238
1214
  var href = location.href.replace(/(javascript:|#).*$/, '');
1239
1215
  location.replace(href + '#' + fragment);
1240
1216
  } else {
1241
- // #1649 - Some browsers require that `hash` contains a leading #.
1217
+ // Some browsers require that `hash` contains a leading #.
1242
1218
  location.hash = '#' + fragment;
1243
1219
  }
1244
1220
  }
@@ -1298,18 +1274,6 @@
1298
1274
  return this;
1299
1275
  },
1300
1276
 
1301
- // For small amounts of DOM Elements, where a full-blown template isn't
1302
- // needed, use **make** to manufacture elements, one at a time.
1303
- //
1304
- // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
1305
- //
1306
- make: function(tagName, attributes, content) {
1307
- var el = document.createElement(tagName);
1308
- if (attributes) Backbone.$(el).attr(attributes);
1309
- if (content != null) Backbone.$(el).html(content);
1310
- return el;
1311
- },
1312
-
1313
1277
  // Change the view's element (`this.el` property), including event
1314
1278
  // re-delegation.
1315
1279
  setElement: function(element, delegate) {
@@ -1347,9 +1311,9 @@
1347
1311
  method = _.bind(method, this);
1348
1312
  eventName += '.delegateEvents' + this.cid;
1349
1313
  if (selector === '') {
1350
- this.$el.bind(eventName, method);
1314
+ this.$el.on(eventName, method);
1351
1315
  } else {
1352
- this.$el.delegate(selector, eventName, method);
1316
+ this.$el.on(eventName, selector, method);
1353
1317
  }
1354
1318
  }
1355
1319
  },
@@ -1358,7 +1322,7 @@
1358
1322
  // You usually don't need to use this, but may wish to if you have multiple
1359
1323
  // Backbone views attached to the same DOM element.
1360
1324
  undelegateEvents: function() {
1361
- this.$el.unbind('.delegateEvents' + this.cid);
1325
+ this.$el.off('.delegateEvents' + this.cid);
1362
1326
  },
1363
1327
 
1364
1328
  // Performs the initial configuration of a View with a set of options.
@@ -1379,7 +1343,8 @@
1379
1343
  var attrs = _.extend({}, _.result(this, 'attributes'));
1380
1344
  if (this.id) attrs.id = _.result(this, 'id');
1381
1345
  if (this.className) attrs['class'] = _.result(this, 'className');
1382
- this.setElement(this.make(_.result(this, 'tagName'), attrs), false);
1346
+ var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
1347
+ this.setElement($el, false);
1383
1348
  } else {
1384
1349
  this.setElement(_.result(this, 'el'), false);
1385
1350
  }
@@ -1461,19 +1426,19 @@
1461
1426
  }
1462
1427
 
1463
1428
  var success = options.success;
1464
- options.success = function(resp, status, xhr) {
1465
- if (success) success(resp, status, xhr);
1429
+ options.success = function(resp) {
1430
+ if (success) success(model, resp, options);
1466
1431
  model.trigger('sync', model, resp, options);
1467
1432
  };
1468
1433
 
1469
1434
  var error = options.error;
1470
- options.error = function(xhr, status, thrown) {
1435
+ options.error = function(xhr) {
1471
1436
  if (error) error(model, xhr, options);
1472
1437
  model.trigger('error', model, xhr, options);
1473
1438
  };
1474
1439
 
1475
1440
  // Make the request, allowing the user to override any Ajax options.
1476
- var xhr = Backbone.ajax(_.extend(params, options));
1441
+ var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
1477
1442
  model.trigger('request', model, xhr, options);
1478
1443
  return xhr;
1479
1444
  };
@@ -1499,7 +1464,7 @@
1499
1464
  if (protoProps && _.has(protoProps, 'constructor')) {
1500
1465
  child = protoProps.constructor;
1501
1466
  } else {
1502
- child = function(){ parent.apply(this, arguments); };
1467
+ child = function(){ return parent.apply(this, arguments); };
1503
1468
  }
1504
1469
 
1505
1470
  // Add static properties to the constructor function, if supplied.
@@ -62,6 +62,18 @@
62
62
  if (complete) complete(jqXHR, textStatus);
63
63
  };
64
64
 
65
+ var success = options.success;
66
+ params.success = function(resp) {
67
+ if (success) success(model, resp, options);
68
+ model.trigger('sync', model, resp, options);
69
+ };
70
+
71
+ var error = options.error;
72
+ params.error = function(xhr) {
73
+ if (error) error(model, xhr, options);
74
+ model.trigger('error', model, xhr, options);
75
+ };
76
+
65
77
  // Make the request.
66
78
  return $.ajax(params);
67
79
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-backbone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.10
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-12-17 00:00:00.000000000 Z
13
+ date: 2013-02-02 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: railties
@@ -242,7 +242,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
242
242
  version: '0'
243
243
  segments:
244
244
  - 0
245
- hash: 1886977708027563026
245
+ hash: 1693904627653384440
246
246
  required_rubygems_version: !ruby/object:Gem::Requirement
247
247
  none: false
248
248
  requirements:
@@ -251,7 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
251
251
  version: '0'
252
252
  segments:
253
253
  - 0
254
- hash: 1886977708027563026
254
+ hash: 1693904627653384440
255
255
  requirements: []
256
256
  rubyforge_project:
257
257
  rubygems_version: 1.8.23