rails-backbone 0.9.10 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6f0079863baa5a023e63ec04c0982831ff1b3ce5
4
+ data.tar.gz: ea4af0b09547e98ac07c65a13b28aa27c74de0dc
5
+ SHA512:
6
+ metadata.gz: 07479dc8a4e9d8426975acb8b559f6e711da0f16d032205fda6fa5eb05b716e39c2e69a62d8bc4d304e306f1b6f737e92537d88ac62dae3a8c00f661270f5bd2
7
+ data.tar.gz: 4b5df6e7138926422c45f5952122943eae345e863f476087ee57cff185c9eb7a512c4b666ea9d80c51abfed328d7a1516de6cd051033e868d7f5f437fc31599b
@@ -1,4 +1,4 @@
1
- Copyright 2011 Ryan Fitzgerald
1
+ Copyright 2011-2014 Ryan Fitzgerald
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,20 +1,45 @@
1
- # Backbone-Rails [![Build Status](https://secure.travis-ci.org/codebrew/backbone-rails.png)](http://travis-ci.org/codebrew/backbone-rails)
1
+ # Backbone-Rails [![Build Status](https://secure.travis-ci.org/codebrew/backbone-rails.png)](http://travis-ci.org/codebrew/backbone-rails)[![Gem Version](https://badge.fury.io/rb/rails-backbone.png)](http://badge.fury.io/rb/rails-backbone)
2
2
 
3
- Easily setup and use backbone.js (0.9.10) with Rails 3.1 and greater
3
+ Easily setup and use backbone.js (1.2.0) with Rails 3.1 and greater
4
4
 
5
- Follow [@TheRyanFitz on Twitter](http://twitter.com/#!/TheRyanFitz). Tweet any questions or suggestions you have about the project.
5
+ ##Version##
6
+
7
+ ###Github master branch###
8
+
9
+ Gem version : 1.2.0
10
+
11
+ Backbone version : 1.2.0
12
+
13
+ Underscore version : 1.8.3
14
+
15
+ ###Rubygems###
16
+
17
+ Gem version : 1.2.0
18
+
19
+ Backbone version : 1.2.0
20
+
21
+ Underscore version : 1.8.3
22
+
23
+
24
+ ##Credits##
25
+ ###Author###
26
+ [Ryan Fitzgerald](http://twitter.com/#!/TheRyanFitz)
27
+ ###Current Maintainer
28
+ [Manu S Ajith](http://twitter.com/manusajith)
29
+ ###Contributors###
30
+ These [awesome people](https://github.com/codebrew/backbone-rails/graphs/contributors) helped to keep this gem updated
6
31
 
7
32
  ## Rails setup
8
33
  This gem requires the use of rails 3.1 and greater, coffeescript and the new rails asset pipeline provided by sprockets.
9
34
 
10
- This gem vendors the latest version of underscore.js and backbone.js for Rails 3.1 and greater. The files will be added to the asset pipeline and available for you to use.
11
-
35
+ This gem vendors the latest version of underscore.js and backbone.js for Rails 3.1 and greater. The files will be added to the asset pipeline and available for you to use.
36
+
12
37
  ### Installation
13
38
 
14
39
  In your Gemfile, add this line:
15
40
 
16
41
  gem "rails-backbone"
17
-
42
+
18
43
  Then run the following commands:
19
44
 
20
45
  bundle install
@@ -23,34 +48,34 @@ Then run the following commands:
23
48
  ### Layout and namespacing
24
49
 
25
50
  Running `rails g backbone:install` will create the following directory structure under `app/assets/javascripts/backbone`:
26
-
51
+
27
52
  routers/
28
53
  models/
29
54
  templates/
30
55
  views/
31
-
56
+
32
57
  It will also create a toplevel app_name.coffee file to setup namespacing and setup initial requires.
33
-
58
+
34
59
  ## Generators
35
- backbone-rails provides 3 simple generators to help get you started using backbone.js with rails 3.1 and greater.
60
+ backbone-rails provides 3 simple generators to help get you started using backbone.js with rails 3.1 and greater.
36
61
  The generators will only create client side code (javascript).
37
62
 
38
63
  ### Model Generator
39
64
 
40
- rails g backbone:model
41
-
65
+ rails g backbone:model model_name [property_name:property_type[,]]
66
+
42
67
  This generator creates a backbone model and collection inside `app/assets/javascript/backbone/models` to be used to talk to the rails backend.
43
68
 
44
69
  ### Routers
45
-
46
- rails g backbone:router
47
-
70
+
71
+ rails g backbone:router model_name [action_name[,]]
72
+
48
73
  This generator creates a backbone router with corresponding views and templates for the given actions provided.
49
74
 
50
75
  ### Scaffolding
51
76
 
52
- rails g backbone:scaffold
53
-
77
+ rails g backbone:scaffold model_name [property_name:property_type[,]]
78
+
54
79
  This generator creates a router, views, templates, model and collection to create a simple crud single page app
55
80
 
56
81
  ## Example Usage
@@ -70,8 +95,8 @@ Install the gem and generate scaffolding.
70
95
  rails g scaffold Post title:string content:string
71
96
  rake db:migrate
72
97
  rails g backbone:scaffold Post title:string content:string
73
-
74
- You now have installed the backbone-rails gem, setup a default directory structure for your frontend backbone code.
98
+
99
+ You now have installed the backbone-rails gem, setup a default directory structure for your frontend backbone code.
75
100
  Then you generated the usual rails server side crud scaffolding and finally generated backbone.js code to provide a simple single page crud app.
76
101
  You have one last step:
77
102
 
@@ -86,9 +111,11 @@ Edit your posts index view `app/views/posts/index.html.erb` with the following c
86
111
  Backbone.history.start();
87
112
  });
88
113
  </script>
89
-
114
+
90
115
  If you prefer haml, this is equivalent to inserting the following code into `app/views/posts/index.html.haml`:
91
116
 
117
+ #posts
118
+
92
119
  :javascript
93
120
  $(function() {
94
121
  // Blog is the app name
@@ -96,6 +123,25 @@ If you prefer haml, this is equivalent to inserting the following code into `app
96
123
  Backbone.history.start();
97
124
  });
98
125
 
99
-
126
+
100
127
  Now start your server `rails s` and browse to [localhost:3000/posts](http://localhost:3000/posts)
101
128
  You should now have a fully functioning single page crud app for Post models.
129
+
130
+ Sample application can be found [here](https://github.com/manusajith/backbone-rails-demo)
131
+
132
+ ##Note:##
133
+ ####Overrides backbone sync function####
134
+ This gem overrides the backbone sync function. Check [here](https://github.com/codebrew/backbone-rails/blob/master/vendor/assets/javascripts/backbone_rails_sync.js) for details.
135
+
136
+ ####With Rails 4:####
137
+ If you are using the default Rails 4 scaffold generators, you will need to adjust the default JSON show view (IE, 'show.json') to render the 'id' attribute.
138
+
139
+ default rails generated show.json.jbuilder
140
+
141
+ `json.extract! @post, :title, :content, :created_at, :updated_at`
142
+
143
+ Change it to add `id` attribute as well
144
+
145
+ `json.extract! @post, :id, :title, :content, :created_at, :updated_at`
146
+
147
+ Without adjusting the JSON show view, you will be redirected to a "undefined" url after creating an object.
data/Rakefile CHANGED
@@ -22,6 +22,7 @@ Rake::TestTask.new(:test) do |t|
22
22
  t.libs << 'lib'
23
23
  t.libs << 'test'
24
24
  t.pattern = 'test/**/*_test.rb'
25
+ t.warning = true
25
26
  t.verbose = false
26
27
  end
27
28
 
@@ -13,14 +13,36 @@ module Backbone
13
13
  :desc => "Skip Git ignores and keeps"
14
14
 
15
15
  def inject_backbone
16
- inject_into_file "app/assets/javascripts/application.js", :before => "//= require_tree" do
17
- "//= require underscore\n//= require backbone\n//= require backbone_rails_sync\n//= require backbone_datalink\n//= require backbone/#{application_name.underscore}\n"
16
+ # for JavaScript application.js manifest:
17
+ if File.exist? "#{Rails.root}/app/assets/javascripts/application.js"
18
+ #add backbone.js files above the require_tree if present
19
+ if File.readlines("#{Rails.root}/app/assets/javascripts/application.js").grep(/require_tree/).any?
20
+ inject_into_file "app/assets/javascripts/application.js", before: '//= require_tree .' do
21
+ "//= require underscore\n//= require backbone\n//= require backbone_rails_sync\n//= require backbone_datalink\n//= require backbone/#{application_name.underscore}\n"
22
+ end
23
+ else
24
+ append_to_file "app/assets/javascripts/application.js" do
25
+ "//= require underscore\n//= require backbone\n//= require backbone_rails_sync\n//= require backbone_datalink\n//= require backbone/#{application_name.underscore}\n"
26
+ end
27
+ end
28
+ # ...or for CoffeeScript application.js.coffee manifest:
29
+ elsif File.exist? "#{Rails.root}/app/assets/javascripts/application.js.coffee"
30
+ #add backbone.js files above the require_tree if present
31
+ if File.readlines("#{Rails.root}/app/assets/javascripts/application.js.coffee").grep(/require_tree/).any?
32
+ inject_into_file "app/assets/javascripts/application.js.coffee", before: '#= require_tree .' do
33
+ "\n#= require underscore\n#= require backbone\n#= require backbone_rails_sync\n#= require backbone_datalink\n#= require backbone/#{application_name.underscore}\n"
34
+ end
35
+ else
36
+ append_to_file "app/assets/javascripts/application.js.coffee" do
37
+ "\n#= require underscore\n#= require backbone\n#= require backbone_rails_sync\n#= require backbone_datalink\n#= require backbone/#{application_name.underscore}\n"
38
+ end
39
+ end
18
40
  end
19
41
  end
20
42
 
21
43
  def create_dir_layout
22
44
  %W{routers models views templates}.each do |dir|
23
- empty_directory "app/assets/javascripts/backbone/#{dir}"
45
+ empty_directory "app/assets/javascripts/backbone/#{dir}"
24
46
  create_file "app/assets/javascripts/backbone/#{dir}/.gitkeep" unless options[:skip_git]
25
47
  end
26
48
  end
@@ -15,7 +15,7 @@ class <%= router_namespace %>Router extends Backbone.Router
15
15
  $("#<%= plural_name %>").html(@view.render().el)
16
16
 
17
17
  index: ->
18
- @view = new <%= "#{view_namespace}.IndexView(#{plural_name}: @#{plural_name})" %>
18
+ @view = new <%= "#{view_namespace}.IndexView(collection: @#{plural_name})" %>
19
19
  $("#<%= plural_name %>").html(@view.render().el)
20
20
 
21
21
  show: (id) ->
@@ -2,6 +2,6 @@
2
2
  <td><%%= <%= attribute.name %> %></td>
3
3
  <% end -%>
4
4
 
5
- <td><a href="#/<%%= id %>">Show</td>
6
- <td><a href="#/<%%= id %>/edit">Edit</td>
7
- <td><a href="#/<%%= id %>/destroy" class="destroy">Destroy</a></td>
5
+ <td><a href="#/<%%= id %>">Show</a></td>
6
+ <td><a href="#/<%%= id %>/edit">Edit</a></td>
7
+ <td><a href="#/<%%= id %>/destroy" class="destroy">Destroy</a></td>
@@ -4,17 +4,17 @@ class <%= view_namespace %>.IndexView extends Backbone.View
4
4
  template: JST["<%= jst 'index' %>"]
5
5
 
6
6
  initialize: () ->
7
- @options.<%= plural_model_name %>.bind('reset', @addAll)
7
+ @collection.bind('reset', @addAll)
8
8
 
9
9
  addAll: () =>
10
- @options.<%= plural_model_name %>.each(@addOne)
10
+ @collection.each(@addOne)
11
11
 
12
12
  addOne: (<%= singular_model_name %>) =>
13
13
  view = new <%= view_namespace %>.<%= singular_name.camelize %>View({model : <%= singular_model_name %>})
14
14
  @$("tbody").append(view.render().el)
15
15
 
16
16
  render: =>
17
- @$el.html(@template(<%= plural_model_name %>: @options.<%= plural_model_name %>.toJSON() ))
17
+ @$el.html(@template(<%= plural_model_name %>: @collection.toJSON() ))
18
18
  @addAll()
19
19
 
20
20
  return this
@@ -1,47 +1,55 @@
1
- // Backbone.js 0.9.10
1
+ // Backbone.js 1.2.0
2
2
 
3
- // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
3
+ // (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4
4
  // Backbone may be freely distributed under the MIT license.
5
5
  // For all details and documentation:
6
6
  // http://backbonejs.org
7
7
 
8
- (function(){
8
+ (function(factory) {
9
+
10
+ // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
11
+ // We use `self` instead of `window` for `WebWorker` support.
12
+ var root = (typeof self == 'object' && self.self == self && self) ||
13
+ (typeof global == 'object' && global.global == global && global);
14
+
15
+ // Set up Backbone appropriately for the environment. Start with AMD.
16
+ if (typeof define === 'function' && define.amd) {
17
+ define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
18
+ // Export global even in AMD case in case this script is loaded with
19
+ // others that may still expect a global Backbone.
20
+ root.Backbone = factory(root, exports, _, $);
21
+ });
22
+
23
+ // Next for Node.js or CommonJS. jQuery may not be needed as a module.
24
+ } else if (typeof exports !== 'undefined') {
25
+ var _ = require('underscore'), $;
26
+ try { $ = require('jquery'); } catch(e) {}
27
+ factory(root, exports, _, $);
28
+
29
+ // Finally, as a browser global.
30
+ } else {
31
+ root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
32
+ }
33
+
34
+ }(function(root, Backbone, _, $) {
9
35
 
10
36
  // Initial Setup
11
37
  // -------------
12
38
 
13
- // Save a reference to the global object (`window` in the browser, `exports`
14
- // on the server).
15
- var root = this;
16
-
17
39
  // Save the previous value of the `Backbone` variable, so that it can be
18
40
  // restored later on, if `noConflict` is used.
19
41
  var previousBackbone = root.Backbone;
20
42
 
21
- // Create a local reference to array methods.
43
+ // Create local references to array methods we'll want to use later.
22
44
  var array = [];
23
- var push = array.push;
24
45
  var slice = array.slice;
25
- var splice = array.splice;
26
-
27
- // The top-level namespace. All public Backbone classes and modules will
28
- // be attached to this. Exported for both CommonJS and the browser.
29
- var Backbone;
30
- if (typeof exports !== 'undefined') {
31
- Backbone = exports;
32
- } else {
33
- Backbone = root.Backbone = {};
34
- }
35
46
 
36
47
  // Current version of the library. Keep in sync with `package.json`.
37
- Backbone.VERSION = '0.9.10';
48
+ Backbone.VERSION = '1.2.0';
38
49
 
39
- // Require Underscore, if we're on the server, and it's not already present.
40
- var _ = root._;
41
- if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
42
-
43
- // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
44
- Backbone.$ = root.jQuery || root.Zepto || root.ender;
50
+ // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
51
+ // the `$` variable.
52
+ Backbone.$ = $;
45
53
 
46
54
  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
47
55
  // to its previous owner. Returns a reference to this Backbone object.
@@ -51,12 +59,12 @@
51
59
  };
52
60
 
53
61
  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
54
- // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
62
+ // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
55
63
  // set a `X-Http-Method-Override` header.
56
64
  Backbone.emulateHTTP = false;
57
65
 
58
66
  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
59
- // `application/json` requests ... will encode the body as
67
+ // `application/json` requests ... this will encode the body as
60
68
  // `application/x-www-form-urlencoded` instead and will send the model in a
61
69
  // form param named `model`.
62
70
  Backbone.emulateJSON = false;
@@ -64,159 +72,289 @@
64
72
  // Backbone.Events
65
73
  // ---------------
66
74
 
75
+ // A module that can be mixed in to *any object* in order to provide it with
76
+ // custom events. You may bind with `on` or remove with `off` callback
77
+ // functions to an event; `trigger`-ing an event fires all callbacks in
78
+ // succession.
79
+ //
80
+ // var object = {};
81
+ // _.extend(object, Backbone.Events);
82
+ // object.on('expand', function(){ alert('expanded'); });
83
+ // object.trigger('expand');
84
+ //
85
+ var Events = Backbone.Events = {};
86
+
67
87
  // Regular expression used to split event strings.
68
88
  var eventSplitter = /\s+/;
69
89
 
70
- // Implement fancy features of the Events API such as multiple event
71
- // names `"change blur"` and jQuery-style event maps `{change: action}`
72
- // in terms of the existing API.
73
- var eventsApi = function(obj, action, name, rest) {
74
- if (!name) return true;
75
- if (typeof name === 'object') {
76
- for (var key in name) {
77
- obj[action].apply(obj, [key, name[key]].concat(rest));
90
+ // Iterates over the standard `event, callback` (as well as the fancy multiple
91
+ // space-separated events `"change blur", callback` and jQuery-style event
92
+ // maps `{event: callback}`), reducing them by manipulating `memo`.
93
+ // Passes a normalized single event name and callback, as well as any
94
+ // optional `opts`.
95
+ var eventsApi = function(iteratee, memo, name, callback, opts) {
96
+ var i = 0, names;
97
+ if (name && typeof name === 'object') {
98
+ // Handle event maps.
99
+ for (names = _.keys(name); i < names.length ; i++) {
100
+ memo = iteratee(memo, names[i], name[names[i]], opts);
78
101
  }
79
- } else if (eventSplitter.test(name)) {
80
- var names = name.split(eventSplitter);
81
- for (var i = 0, l = names.length; i < l; i++) {
82
- obj[action].apply(obj, [names[i]].concat(rest));
102
+ } else if (name && eventSplitter.test(name)) {
103
+ // Handle space separated event names.
104
+ for (names = name.split(eventSplitter); i < names.length; i++) {
105
+ memo = iteratee(memo, names[i], callback, opts);
83
106
  }
84
107
  } else {
85
- return true;
108
+ memo = iteratee(memo, name, callback, opts);
86
109
  }
110
+ return memo;
87
111
  };
88
112
 
89
- // Optimized internal dispatch function for triggering events. Tries to
90
- // keep the usual cases speedy (most Backbone events have 3 arguments).
91
- var triggerEvents = function(events, args) {
92
- var ev, i = -1, l = events.length;
93
- switch (args.length) {
94
- case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
95
- return;
96
- case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
97
- return;
98
- case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
99
- return;
100
- case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
101
- return;
102
- default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
113
+ // Bind an event to a `callback` function. Passing `"all"` will bind
114
+ // the callback to all events fired.
115
+ Events.on = function(name, callback, context) {
116
+ return internalOn(this, name, callback, context);
117
+ };
118
+
119
+ // An internal use `on` function, used to guard the `listening` argument from
120
+ // the public API.
121
+ var internalOn = function(obj, name, callback, context, listening) {
122
+ obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
123
+ context: context,
124
+ ctx: obj,
125
+ listening: listening
126
+ });
127
+
128
+ if (listening) {
129
+ var listeners = obj._listeners || (obj._listeners = {});
130
+ listeners[listening.id] = listening;
103
131
  }
132
+
133
+ return obj;
104
134
  };
105
135
 
106
- // A module that can be mixed in to *any object* in order to provide it with
107
- // custom events. You may bind with `on` or remove with `off` callback
108
- // functions to an event; `trigger`-ing an event fires all callbacks in
109
- // succession.
110
- //
111
- // var object = {};
112
- // _.extend(object, Backbone.Events);
113
- // object.on('expand', function(){ alert('expanded'); });
114
- // object.trigger('expand');
115
- //
116
- var Events = Backbone.Events = {
117
-
118
- // Bind one or more space separated events, or an events map,
119
- // to a `callback` function. Passing `"all"` will bind the callback to
120
- // all events fired.
121
- on: function(name, callback, context) {
122
- if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
123
- this._events || (this._events = {});
124
- var list = this._events[name] || (this._events[name] = []);
125
- list.push({callback: callback, context: context, ctx: context || this});
126
- return this;
127
- },
136
+ // Inversion-of-control versions of `on`. Tell *this* object to listen to
137
+ // an event in another object... keeping track of what it's listening to.
138
+ Events.listenTo = function(obj, name, callback) {
139
+ if (!obj) return this;
140
+ var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
141
+ var listeningTo = this._listeningTo || (this._listeningTo = {});
142
+ var listening = listeningTo[id];
143
+
144
+ // This object is not listening to any other events on `obj` yet.
145
+ // Setup the necessary references to track the listening callbacks.
146
+ if (!listening) {
147
+ var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
148
+ listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
149
+ }
128
150
 
129
- // Bind events to only be triggered a single time. After the first time
130
- // the callback is invoked, it will be removed.
131
- once: function(name, callback, context) {
132
- if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this;
133
- var self = this;
134
- var once = _.once(function() {
135
- self.off(name, once);
136
- callback.apply(this, arguments);
137
- });
138
- once._callback = callback;
139
- this.on(name, once, context);
140
- return this;
141
- },
151
+ // Bind callbacks on obj, and keep track of them on listening.
152
+ internalOn(obj, name, callback, this, listening);
153
+ return this;
154
+ };
155
+
156
+ // The reducing API that adds a callback to the `events` object.
157
+ var onApi = function(events, name, callback, options) {
158
+ if (callback) {
159
+ var handlers = events[name] || (events[name] = []);
160
+ var context = options.context, ctx = options.ctx, listening = options.listening;
161
+ if (listening) listening.count++;
162
+
163
+ handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });
164
+ }
165
+ return events;
166
+ };
167
+
168
+ // Remove one or many callbacks. If `context` is null, removes all
169
+ // callbacks with that function. If `callback` is null, removes all
170
+ // callbacks for the event. If `name` is null, removes all bound
171
+ // callbacks for all events.
172
+ Events.off = function(name, callback, context) {
173
+ if (!this._events) return this;
174
+ this._events = eventsApi(offApi, this._events, name, callback, {
175
+ context: context,
176
+ listeners: this._listeners
177
+ });
178
+ return this;
179
+ };
180
+
181
+ // Tell this object to stop listening to either specific events ... or
182
+ // to every object it's currently listening to.
183
+ Events.stopListening = function(obj, name, callback) {
184
+ var listeningTo = this._listeningTo;
185
+ if (!listeningTo) return this;
186
+
187
+ var ids = obj ? [obj._listenId] : _.keys(listeningTo);
188
+
189
+ for (var i = 0; i < ids.length; i++) {
190
+ var listening = listeningTo[ids[i]];
191
+
192
+ // If listening doesn't exist, this object is not currently
193
+ // listening to obj. Break out early.
194
+ if (!listening) break;
195
+
196
+ listening.obj.off(name, callback, this);
197
+ }
198
+ if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
199
+
200
+ return this;
201
+ };
142
202
 
143
- // Remove one or many callbacks. If `context` is null, removes all
144
- // callbacks with that function. If `callback` is null, removes all
145
- // callbacks for the event. If `name` is null, removes all bound
146
- // callbacks for all events.
147
- off: function(name, callback, context) {
148
- var list, ev, events, names, i, l, j, k;
149
- if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
150
- if (!name && !callback && !context) {
151
- this._events = {};
152
- return this;
203
+ // The reducing API that removes a callback from the `events` object.
204
+ var offApi = function(events, name, callback, options) {
205
+ // No events to consider.
206
+ if (!events) return;
207
+
208
+ var i = 0, length, listening;
209
+ var context = options.context, listeners = options.listeners;
210
+
211
+ // Delete all events listeners and "drop" events.
212
+ if (!name && !callback && !context) {
213
+ var ids = _.keys(listeners);
214
+ for (; i < ids.length; i++) {
215
+ listening = listeners[ids[i]];
216
+ delete listeners[listening.id];
217
+ delete listening.listeningTo[listening.objId];
153
218
  }
219
+ return;
220
+ }
154
221
 
155
- names = name ? [name] : _.keys(this._events);
156
- for (i = 0, l = names.length; i < l; i++) {
157
- name = names[i];
158
- if (list = this._events[name]) {
159
- events = [];
160
- if (callback || context) {
161
- for (j = 0, k = list.length; j < k; j++) {
162
- ev = list[j];
163
- if ((callback && callback !== ev.callback &&
164
- callback !== ev.callback._callback) ||
165
- (context && context !== ev.context)) {
166
- events.push(ev);
167
- }
168
- }
222
+ var names = name ? [name] : _.keys(events);
223
+ for (; i < names.length; i++) {
224
+ name = names[i];
225
+ var handlers = events[name];
226
+
227
+ // Bail out if there are no events stored.
228
+ if (!handlers) break;
229
+
230
+ // Replace events if there are any remaining. Otherwise, clean up.
231
+ var remaining = [];
232
+ for (var j = 0; j < handlers.length; j++) {
233
+ var handler = handlers[j];
234
+ if (
235
+ callback && callback !== handler.callback &&
236
+ callback !== handler.callback._callback ||
237
+ context && context !== handler.context
238
+ ) {
239
+ remaining.push(handler);
240
+ } else {
241
+ listening = handler.listening;
242
+ if (listening && --listening.count === 0) {
243
+ delete listeners[listening.id];
244
+ delete listening.listeningTo[listening.objId];
169
245
  }
170
- this._events[name] = events;
171
246
  }
172
247
  }
173
248
 
174
- return this;
175
- },
249
+ // Update tail event if the list has any events. Otherwise, clean up.
250
+ if (remaining.length) {
251
+ events[name] = remaining;
252
+ } else {
253
+ delete events[name];
254
+ }
255
+ }
256
+ if (_.size(events)) return events;
257
+ };
258
+
259
+ // Bind an event to only be triggered a single time. After the first time
260
+ // the callback is invoked, it will be removed. When multiple events are
261
+ // passed in using the space-separated syntax, the event will fire once for every
262
+ // event you passed in, not once for a combination of all events
263
+ Events.once = function(name, callback, context) {
264
+ // Map the event into a `{event: once}` object.
265
+ var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
266
+ return this.on(events, void 0, context);
267
+ };
268
+
269
+ // Inversion-of-control versions of `once`.
270
+ Events.listenToOnce = function(obj, name, callback) {
271
+ // Map the event into a `{event: once}` object.
272
+ var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
273
+ return this.listenTo(obj, events);
274
+ };
176
275
 
177
- // Trigger one or many events, firing all bound callbacks. Callbacks are
178
- // passed the same arguments as `trigger` is, apart from the event name
179
- // (unless you're listening on `"all"`, which will cause your callback to
180
- // receive the true name of the event as the first argument).
181
- trigger: function(name) {
182
- if (!this._events) return this;
183
- var args = slice.call(arguments, 1);
184
- if (!eventsApi(this, 'trigger', name, args)) return this;
185
- var events = this._events[name];
186
- var allEvents = this._events.all;
276
+ // Reduces the event callbacks into a map of `{event: onceWrapper}`.
277
+ // `offer` unbinds the `onceWrapper` after it as been called.
278
+ var onceMap = function(map, name, callback, offer) {
279
+ if (callback) {
280
+ var once = map[name] = _.once(function() {
281
+ offer(name, once);
282
+ callback.apply(this, arguments);
283
+ });
284
+ once._callback = callback;
285
+ }
286
+ return map;
287
+ };
288
+
289
+ // Trigger one or many events, firing all bound callbacks. Callbacks are
290
+ // passed the same arguments as `trigger` is, apart from the event name
291
+ // (unless you're listening on `"all"`, which will cause your callback to
292
+ // receive the true name of the event as the first argument).
293
+ Events.trigger = function(name) {
294
+ if (!this._events) return this;
295
+
296
+ var length = Math.max(0, arguments.length - 1);
297
+ var args = Array(length);
298
+ for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
299
+
300
+ eventsApi(triggerApi, this._events, name, void 0, args);
301
+ return this;
302
+ };
303
+
304
+ // Handles triggering the appropriate event callbacks.
305
+ var triggerApi = function(objEvents, name, cb, args) {
306
+ if (objEvents) {
307
+ var events = objEvents[name];
308
+ var allEvents = objEvents.all;
309
+ if (events && allEvents) allEvents = allEvents.slice();
187
310
  if (events) triggerEvents(events, args);
188
- if (allEvents) triggerEvents(allEvents, arguments);
189
- return this;
190
- },
311
+ if (allEvents) triggerEvents(allEvents, [name].concat(args));
312
+ }
313
+ return objEvents;
314
+ };
191
315
 
192
- // An inversion-of-control version of `on`. Tell *this* object to listen to
193
- // an event in another object ... keeping track of what it's listening to.
194
- listenTo: function(obj, name, callback) {
195
- var listeners = this._listeners || (this._listeners = {});
196
- var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
197
- listeners[id] = obj;
198
- obj.on(name, typeof name === 'object' ? this : callback, this);
199
- return this;
200
- },
316
+ // A difficult-to-believe, but optimized internal dispatch function for
317
+ // triggering events. Tries to keep the usual cases speedy (most internal
318
+ // Backbone events have 3 arguments).
319
+ var triggerEvents = function(events, args) {
320
+ var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
321
+ switch (args.length) {
322
+ case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
323
+ case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
324
+ case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
325
+ case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
326
+ default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
327
+ }
328
+ };
201
329
 
202
- // Tell this object to stop listening to either specific events ... or
203
- // to every object it's currently listening to.
204
- stopListening: function(obj, name, callback) {
205
- var listeners = this._listeners;
206
- if (!listeners) return;
207
- if (obj) {
208
- obj.off(name, typeof name === 'object' ? this : callback, this);
209
- if (!name && !callback) delete listeners[obj._listenerId];
210
- } else {
211
- if (typeof name === 'object') callback = this;
212
- for (var id in listeners) {
213
- listeners[id].off(name, callback, this);
214
- }
215
- this._listeners = {};
216
- }
217
- return this;
330
+ // Proxy Underscore methods to a Backbone class' prototype using a
331
+ // particular attribute as the data argument
332
+ var addMethod = function(length, method, attribute) {
333
+ switch (length) {
334
+ case 1: return function() {
335
+ return _[method](this[attribute]);
336
+ };
337
+ case 2: return function(value) {
338
+ return _[method](this[attribute], value);
339
+ };
340
+ case 3: return function(iteratee, context) {
341
+ return _[method](this[attribute], iteratee, context);
342
+ };
343
+ case 4: return function(iteratee, defaultVal, context) {
344
+ return _[method](this[attribute], iteratee, defaultVal, context);
345
+ };
346
+ default: return function() {
347
+ var args = slice.call(arguments);
348
+ args.unshift(this[attribute]);
349
+ return _[method].apply(_, args);
350
+ };
218
351
  }
219
352
  };
353
+ var addUnderscoreMethods = function(Class, methods, attribute) {
354
+ _.each(methods, function(length, method) {
355
+ if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
356
+ });
357
+ };
220
358
 
221
359
  // Aliases for backwards compatibility.
222
360
  Events.bind = Events.on;
@@ -229,18 +367,21 @@
229
367
  // Backbone.Model
230
368
  // --------------
231
369
 
232
- // Create a new model, with defined attributes. A client id (`cid`)
370
+ // Backbone **Models** are the basic data object in the framework --
371
+ // frequently representing a row in a table in a database on your server.
372
+ // A discrete chunk of data and a bunch of useful, related methods for
373
+ // performing computations and transformations on that data.
374
+
375
+ // Create a new model with the specified attributes. A client id (`cid`)
233
376
  // is automatically generated and assigned for you.
234
377
  var Model = Backbone.Model = function(attributes, options) {
235
- var defaults;
236
378
  var attrs = attributes || {};
237
- this.cid = _.uniqueId('c');
379
+ options || (options = {});
380
+ this.cid = _.uniqueId(this.cidPrefix);
238
381
  this.attributes = {};
239
- if (options && options.collection) this.collection = options.collection;
240
- if (options && options.parse) attrs = this.parse(attrs, options) || {};
241
- if (defaults = _.result(this, 'defaults')) {
242
- attrs = _.defaults({}, attrs, defaults);
243
- }
382
+ if (options.collection) this.collection = options.collection;
383
+ if (options.parse) attrs = this.parse(attrs, options) || {};
384
+ attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
244
385
  this.set(attrs, options);
245
386
  this.changed = {};
246
387
  this.initialize.apply(this, arguments);
@@ -252,10 +393,17 @@
252
393
  // A hash of attributes whose current and previous value differ.
253
394
  changed: null,
254
395
 
396
+ // The value returned during the last failed validation.
397
+ validationError: null,
398
+
255
399
  // The default name for the JSON `id` attribute is `"id"`. MongoDB and
256
400
  // CouchDB users may want to set this to `"_id"`.
257
401
  idAttribute: 'id',
258
402
 
403
+ // The prefix is used to create the client id which is used to identify models locally.
404
+ // You may want to override this if you're experiencing name clashes with model ids.
405
+ cidPrefix: 'c',
406
+
259
407
  // Initialize is an empty function by default. Override it with your own
260
408
  // initialization logic.
261
409
  initialize: function(){},
@@ -265,7 +413,8 @@
265
413
  return _.clone(this.attributes);
266
414
  },
267
415
 
268
- // Proxy `Backbone.sync` by default.
416
+ // Proxy `Backbone.sync` by default -- but override this if you need
417
+ // custom syncing semantics for *this* particular model.
269
418
  sync: function() {
270
419
  return Backbone.sync.apply(this, arguments);
271
420
  },
@@ -286,10 +435,14 @@
286
435
  return this.get(attr) != null;
287
436
  },
288
437
 
289
- // ----------------------------------------------------------------------
438
+ // Special-cased proxy to underscore's `_.matches` method.
439
+ matches: function(attrs) {
440
+ return !!_.iteratee(attrs, this)(this.attributes);
441
+ },
290
442
 
291
- // Set a hash of model attributes on the object, firing `"change"` unless
292
- // you choose to silence it.
443
+ // Set a hash of model attributes on the object, firing `"change"`. This is
444
+ // the core primitive operation of a model, updating the data and notifying
445
+ // anyone who needs to know about the change in state. The heart of the beast.
293
446
  set: function(key, val, options) {
294
447
  var attr, attrs, unset, changes, silent, changing, prev, current;
295
448
  if (key == null) return this;
@@ -337,15 +490,18 @@
337
490
 
338
491
  // Trigger all relevant attribute changes.
339
492
  if (!silent) {
340
- if (changes.length) this._pending = true;
341
- for (var i = 0, l = changes.length; i < l; i++) {
493
+ if (changes.length) this._pending = options;
494
+ for (var i = 0; i < changes.length; i++) {
342
495
  this.trigger('change:' + changes[i], this, current[changes[i]], options);
343
496
  }
344
497
  }
345
498
 
499
+ // You might be wondering why there's a `while` loop here. Changes can
500
+ // be recursively nested within `"change"` events.
346
501
  if (changing) return this;
347
502
  if (!silent) {
348
503
  while (this._pending) {
504
+ options = this._pending;
349
505
  this._pending = false;
350
506
  this.trigger('change', this, options);
351
507
  }
@@ -355,14 +511,13 @@
355
511
  return this;
356
512
  },
357
513
 
358
- // Remove an attribute from the model, firing `"change"` unless you choose
359
- // to silence it. `unset` is a noop if the attribute doesn't exist.
514
+ // Remove an attribute from the model, firing `"change"`. `unset` is a noop
515
+ // if the attribute doesn't exist.
360
516
  unset: function(attr, options) {
361
517
  return this.set(attr, void 0, _.extend({}, options, {unset: true}));
362
518
  },
363
519
 
364
- // Clear all attributes on the model, firing `"change"` unless you choose
365
- // to silence it.
520
+ // Clear all attributes on the model, firing `"change"`.
366
521
  clear: function(options) {
367
522
  var attrs = {};
368
523
  for (var key in this.attributes) attrs[key] = void 0;
@@ -406,19 +561,19 @@
406
561
  return _.clone(this._previousAttributes);
407
562
  },
408
563
 
409
- // ---------------------------------------------------------------------
410
-
411
- // Fetch the model from the server. If the server's representation of the
412
- // model differs from its current attributes, they will be overriden,
413
- // triggering a `"change"` event.
564
+ // Fetch the model from the server, merging the response with the model's
565
+ // local attributes. Any changed attributes will trigger a "change" event.
414
566
  fetch: function(options) {
415
567
  options = options ? _.clone(options) : {};
416
568
  if (options.parse === void 0) options.parse = true;
569
+ var model = this;
417
570
  var success = options.success;
418
- options.success = function(model, resp, options) {
571
+ options.success = function(resp) {
419
572
  if (!model.set(model.parse(resp, options), options)) return false;
420
- if (success) success(model, resp, options);
573
+ if (success) success.call(options.context, model, resp, options);
574
+ model.trigger('sync', model, resp, options);
421
575
  };
576
+ wrapError(this, options);
422
577
  return this.sync('read', this, options);
423
578
  },
424
579
 
@@ -426,7 +581,7 @@
426
581
  // If the server returns an attributes hash that differs, the model's
427
582
  // state will be `set` again.
428
583
  save: function(key, val, options) {
429
- var attrs, success, method, xhr, attributes = this.attributes;
584
+ var attrs, method, xhr, attributes = this.attributes, wait;
430
585
 
431
586
  // Handle both `"key", value` and `{key: value}` -style arguments.
432
587
  if (key == null || typeof key === 'object') {
@@ -436,41 +591,47 @@
436
591
  (attrs = {})[key] = val;
437
592
  }
438
593
 
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;
441
-
442
594
  options = _.extend({validate: true}, options);
595
+ wait = options.wait;
443
596
 
444
- // Do not persist invalid models.
445
- if (!this._validate(attrs, options)) return false;
597
+ // If we're not waiting and attributes exist, save acts as
598
+ // `set(attr).save(null, opts)` with validation. Otherwise, check if
599
+ // the model will be valid when the attributes, if any, are set.
600
+ if (attrs && !wait) {
601
+ if (!this.set(attrs, options)) return false;
602
+ } else {
603
+ if (!this._validate(attrs, options)) return false;
604
+ }
446
605
 
447
606
  // Set temporary attributes if `{wait: true}`.
448
- if (attrs && options.wait) {
607
+ if (attrs && wait) {
449
608
  this.attributes = _.extend({}, attributes, attrs);
450
609
  }
451
610
 
452
611
  // After a successful server-side save, the client is (optionally)
453
612
  // updated with the server-side state.
454
613
  if (options.parse === void 0) options.parse = true;
455
- success = options.success;
456
- options.success = function(model, resp, options) {
614
+ var model = this;
615
+ var success = options.success;
616
+ options.success = function(resp) {
457
617
  // Ensure attributes are restored during synchronous saves.
458
618
  model.attributes = attributes;
459
- var serverAttrs = model.parse(resp, options);
460
- if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
619
+ var serverAttrs = options.parse ? model.parse(resp, options) : resp;
620
+ if (wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
461
621
  if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
462
622
  return false;
463
623
  }
464
- if (success) success(model, resp, options);
624
+ if (success) success.call(options.context, model, resp, options);
625
+ model.trigger('sync', model, resp, options);
465
626
  };
627
+ wrapError(this, options);
466
628
 
467
- // Finish configuring and sending the Ajax request.
468
629
  method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
469
- if (method === 'patch') options.attrs = attrs;
630
+ if (method === 'patch' && !options.attrs) options.attrs = attrs;
470
631
  xhr = this.sync(method, this, options);
471
632
 
472
633
  // Restore attributes.
473
- if (attrs && options.wait) this.attributes = attributes;
634
+ if (attrs && wait) this.attributes = attributes;
474
635
 
475
636
  return xhr;
476
637
  },
@@ -482,23 +643,27 @@
482
643
  options = options ? _.clone(options) : {};
483
644
  var model = this;
484
645
  var success = options.success;
646
+ var wait = options.wait;
485
647
 
486
648
  var destroy = function() {
649
+ model.stopListening();
487
650
  model.trigger('destroy', model, model.collection, options);
488
651
  };
489
652
 
490
- options.success = function(model, resp, options) {
491
- if (options.wait || model.isNew()) destroy();
492
- if (success) success(model, resp, options);
653
+ options.success = function(resp) {
654
+ if (wait) destroy();
655
+ if (success) success.call(options.context, model, resp, options);
656
+ if (!model.isNew()) model.trigger('sync', model, resp, options);
493
657
  };
494
658
 
659
+ var xhr = false;
495
660
  if (this.isNew()) {
496
- options.success(this, null, options);
497
- return false;
661
+ _.defer(options.success);
662
+ } else {
663
+ wrapError(this, options);
664
+ xhr = this.sync('delete', this, options);
498
665
  }
499
-
500
- var xhr = this.sync('delete', this, options);
501
- if (!options.wait) destroy();
666
+ if (!wait) destroy();
502
667
  return xhr;
503
668
  },
504
669
 
@@ -506,9 +671,13 @@
506
671
  // using Backbone's restful methods, override this to change the endpoint
507
672
  // that will be called.
508
673
  url: function() {
509
- var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
674
+ var base =
675
+ _.result(this, 'urlRoot') ||
676
+ _.result(this.collection, 'url') ||
677
+ urlError();
510
678
  if (this.isNew()) return base;
511
- return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
679
+ var id = this.id || this.attributes[this.idAttribute];
680
+ return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(id);
512
681
  },
513
682
 
514
683
  // **parse** converts a response into the hash of attributes to be `set` on
@@ -524,44 +693,60 @@
524
693
 
525
694
  // A model is new if it has never been saved to the server, and lacks an id.
526
695
  isNew: function() {
527
- return this.id == null;
696
+ return !this.has(this.idAttribute);
528
697
  },
529
698
 
530
699
  // Check if the model is currently in a valid state.
531
700
  isValid: function(options) {
532
- return !this.validate || !this.validate(this.attributes, options);
701
+ return this._validate({}, _.extend(options || {}, { validate: true }));
533
702
  },
534
703
 
535
704
  // Run validation against the next complete set of model attributes,
536
- // returning `true` if all is well. Otherwise, fire a general
537
- // `"error"` event and call the error callback, if specified.
705
+ // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
538
706
  _validate: function(attrs, options) {
539
707
  if (!options.validate || !this.validate) return true;
540
708
  attrs = _.extend({}, this.attributes, attrs);
541
709
  var error = this.validationError = this.validate(attrs, options) || null;
542
710
  if (!error) return true;
543
- this.trigger('invalid', this, error, options || {});
711
+ this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
544
712
  return false;
545
713
  }
546
714
 
547
715
  });
548
716
 
717
+ // Underscore methods that we want to implement on the Model.
718
+ var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
719
+ omit: 0, chain: 1, isEmpty: 1 };
720
+
721
+ // Mix in each Underscore method as a proxy to `Model#attributes`.
722
+ addUnderscoreMethods(Model, modelMethods, 'attributes');
723
+
549
724
  // Backbone.Collection
550
725
  // -------------------
551
726
 
552
- // Provides a standard collection class for our sets of models, ordered
553
- // or unordered. If a `comparator` is specified, the Collection will maintain
727
+ // If models tend to represent a single row of data, a Backbone Collection is
728
+ // more analogous to a table full of data ... or a small slice or page of that
729
+ // table, or a collection of rows that belong together for a particular reason
730
+ // -- all of the messages in this particular folder, all of the documents
731
+ // belonging to this particular author, and so on. Collections maintain
732
+ // indexes of their models, both in order, and for lookup by `id`.
733
+
734
+ // Create a new **Collection**, perhaps to contain a specific type of `model`.
735
+ // If a `comparator` is specified, the Collection will maintain
554
736
  // its models in sort order, as they're added and removed.
555
737
  var Collection = Backbone.Collection = function(models, options) {
556
738
  options || (options = {});
557
739
  if (options.model) this.model = options.model;
558
740
  if (options.comparator !== void 0) this.comparator = options.comparator;
559
- this.models = [];
560
741
  this._reset();
561
742
  this.initialize.apply(this, arguments);
562
743
  if (models) this.reset(models, _.extend({silent: true}, options));
563
744
  };
564
745
 
746
+ // Default options for `Collection#set`.
747
+ var setOptions = {add: true, remove: true, merge: true};
748
+ var addOptions = {add: true, remove: false};
749
+
565
750
  // Define the Collection's inheritable methods.
566
751
  _.extend(Collection.prototype, Events, {
567
752
 
@@ -586,96 +771,140 @@
586
771
 
587
772
  // Add a model, or list of models to the set.
588
773
  add: function(models, options) {
589
- models = _.isArray(models) ? models.slice() : [models];
774
+ return this.set(models, _.extend({merge: false}, options, addOptions));
775
+ },
776
+
777
+ // Remove a model, or a list of models from the set.
778
+ remove: function(models, options) {
779
+ var singular = !_.isArray(models), removed;
780
+ models = singular ? [models] : _.clone(models);
590
781
  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;
782
+ removed = this._removeModels(models, options);
783
+ if (!options.silent && removed) this.trigger('update', this, options);
784
+ return singular ? models[0] : models;
785
+ },
786
+
787
+ // Update a collection by `set`-ing a new list of models, adding new ones,
788
+ // removing models that are no longer present, and merging models that
789
+ // already exist in the collection, as necessary. Similar to **Model#set**,
790
+ // the core operation for updating the data contained by the collection.
791
+ set: function(models, options) {
792
+ options = _.defaults({}, options, setOptions);
793
+ if (options.parse) models = this.parse(models, options);
794
+ var singular = !_.isArray(models);
795
+ models = singular ? (models ? [models] : []) : models.slice();
796
+ var id, model, attrs, existing, sort;
797
+ var at = options.at;
798
+ if (at != null) at = +at;
799
+ if (at < 0) at += this.length + 1;
800
+ var sortable = this.comparator && (at == null) && options.sort !== false;
801
+ var sortAttr = _.isString(this.comparator) ? this.comparator : null;
802
+ var toAdd = [], toRemove = [], modelMap = {};
803
+ var add = options.add, merge = options.merge, remove = options.remove;
804
+ var order = !sortable && add && remove ? [] : false;
805
+ var orderChanged = false;
596
806
 
597
807
  // Turn bare objects into model references, and prevent invalid models
598
808
  // from being added.
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);
602
- continue;
603
- }
809
+ for (var i = 0; i < models.length; i++) {
810
+ attrs = models[i];
604
811
 
605
812
  // If a duplicate is found, prevent it from being added and
606
813
  // optionally merge it into the existing model.
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;
814
+ if (existing = this.get(attrs)) {
815
+ if (remove) modelMap[existing.cid] = true;
816
+ if (merge && attrs !== existing) {
817
+ attrs = this._isModel(attrs) ? attrs.attributes : attrs;
818
+ if (options.parse) attrs = existing.parse(attrs, options);
819
+ existing.set(attrs, options);
820
+ if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
611
821
  }
612
- continue;
822
+ models[i] = existing;
823
+
824
+ // If this is a new, valid model, push it to the `toAdd` list.
825
+ } else if (add) {
826
+ model = models[i] = this._prepareModel(attrs, options);
827
+ if (!model) continue;
828
+ toAdd.push(model);
829
+ this._addReference(model, options);
830
+ }
831
+
832
+ // Do not add multiple models with the same `id`.
833
+ model = existing || model;
834
+ if (!model) continue;
835
+ id = this.modelId(model.attributes);
836
+ if (order && (model.isNew() || !modelMap[id])) {
837
+ order.push(model);
838
+
839
+ // Check to see if this is actually a new model at this index.
840
+ orderChanged = orderChanged || !this.models[i] || model.cid !== this.models[i].cid;
613
841
  }
614
842
 
615
- // This is a new model, push it to the `add` list.
616
- add.push(model);
843
+ modelMap[id] = true;
844
+ }
617
845
 
618
- // Listen to added models' events, and index models for lookup by
619
- // `id` and by `cid`.
620
- model.on('all', this._onModelEvent, this);
621
- this._byId[model.cid] = model;
622
- if (model.id != null) this._byId[model.id] = model;
846
+ // Remove nonexistent models if appropriate.
847
+ if (remove) {
848
+ for (var i = 0; i < this.length; i++) {
849
+ if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
850
+ }
851
+ if (toRemove.length) this._removeModels(toRemove, options);
623
852
  }
624
853
 
625
854
  // See if sorting is needed, update `length` and splice in new models.
626
- if (add.length) {
627
- if (sort) doSort = true;
628
- this.length += add.length;
855
+ if (toAdd.length || orderChanged) {
856
+ if (sortable) sort = true;
857
+ this.length += toAdd.length;
629
858
  if (at != null) {
630
- splice.apply(this.models, [at, 0].concat(add));
859
+ for (var i = 0; i < toAdd.length; i++) {
860
+ this.models.splice(at + i, 0, toAdd[i]);
861
+ }
631
862
  } else {
632
- push.apply(this.models, add);
863
+ if (order) this.models.length = 0;
864
+ var orderedModels = order || toAdd;
865
+ for (var i = 0; i < orderedModels.length; i++) {
866
+ this.models.push(orderedModels[i]);
867
+ }
633
868
  }
634
869
  }
635
870
 
636
871
  // Silently sort the collection if appropriate.
637
- if (doSort) this.sort({silent: true});
638
-
639
- if (options.silent) return this;
640
-
641
- // Trigger `add` events.
642
- for (i = 0, l = add.length; i < l; i++) {
643
- (model = add[i]).trigger('add', model, this, options);
872
+ if (sort) this.sort({silent: true});
873
+
874
+ // Unless silenced, it's time to fire all appropriate add/sort events.
875
+ if (!options.silent) {
876
+ var addOpts = at != null ? _.clone(options) : options;
877
+ for (var i = 0; i < toAdd.length; i++) {
878
+ if (at != null) addOpts.index = at + i;
879
+ (model = toAdd[i]).trigger('add', model, this, addOpts);
880
+ }
881
+ if (sort || orderChanged) this.trigger('sort', this, options);
882
+ if (toAdd.length || toRemove.length) this.trigger('update', this, options);
644
883
  }
645
884
 
646
- // Trigger `sort` if the collection was sorted.
647
- if (doSort) this.trigger('sort', this, options);
648
-
649
- return this;
885
+ // Return the added (or merged) model (or models).
886
+ return singular ? models[0] : models;
650
887
  },
651
888
 
652
- // Remove a model, or a list of models from the set.
653
- remove: function(models, options) {
654
- models = _.isArray(models) ? models.slice() : [models];
655
- options || (options = {});
656
- var i, l, index, model;
657
- for (i = 0, l = models.length; i < l; i++) {
658
- model = this.get(models[i]);
659
- if (!model) continue;
660
- delete this._byId[model.id];
661
- delete this._byId[model.cid];
662
- index = this.indexOf(model);
663
- this.models.splice(index, 1);
664
- this.length--;
665
- if (!options.silent) {
666
- options.index = index;
667
- model.trigger('remove', model, this, options);
668
- }
669
- this._removeReference(model);
889
+ // When you have more items than you want to add or remove individually,
890
+ // you can reset the entire set with a new list of models, without firing
891
+ // any granular `add` or `remove` events. Fires `reset` when finished.
892
+ // Useful for bulk operations and optimizations.
893
+ reset: function(models, options) {
894
+ options = options ? _.clone(options) : {};
895
+ for (var i = 0; i < this.models.length; i++) {
896
+ this._removeReference(this.models[i], options);
670
897
  }
671
- return this;
898
+ options.previousModels = this.models;
899
+ this._reset();
900
+ models = this.add(models, _.extend({silent: true}, options));
901
+ if (!options.silent) this.trigger('reset', this, options);
902
+ return models;
672
903
  },
673
904
 
674
905
  // Add a model to the end of the collection.
675
906
  push: function(model, options) {
676
- model = this._prepareModel(model, options);
677
- this.add(model, _.extend({at: this.length}, options));
678
- return model;
907
+ return this.add(model, _.extend({at: this.length}, options));
679
908
  },
680
909
 
681
910
  // Remove a model from the end of the collection.
@@ -687,9 +916,7 @@
687
916
 
688
917
  // Add a model to the beginning of the collection.
689
918
  unshift: function(model, options) {
690
- model = this._prepareModel(model, options);
691
- this.add(model, _.extend({at: 0}, options));
692
- return model;
919
+ return this.add(model, _.extend({at: 0}, options));
693
920
  },
694
921
 
695
922
  // Remove a model from the beginning of the collection.
@@ -700,40 +927,43 @@
700
927
  },
701
928
 
702
929
  // Slice out a sub-array of models from the collection.
703
- slice: function(begin, end) {
704
- return this.models.slice(begin, end);
930
+ slice: function() {
931
+ return slice.apply(this.models, arguments);
705
932
  },
706
933
 
707
934
  // Get a model from the set by id.
708
935
  get: function(obj) {
709
936
  if (obj == null) return void 0;
710
- this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
711
- return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
937
+ var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
938
+ return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
712
939
  },
713
940
 
714
941
  // Get the model at the given index.
715
942
  at: function(index) {
943
+ if (index < 0) index += this.length;
716
944
  return this.models[index];
717
945
  },
718
946
 
719
- // Return models with matching attributes. Useful for simple cases of `filter`.
720
- where: function(attrs) {
721
- if (_.isEmpty(attrs)) return [];
722
- return this.filter(function(model) {
723
- for (var key in attrs) {
724
- if (attrs[key] !== model.get(key)) return false;
725
- }
726
- return true;
947
+ // Return models with matching attributes. Useful for simple cases of
948
+ // `filter`.
949
+ where: function(attrs, first) {
950
+ var matches = _.matches(attrs);
951
+ return this[first ? 'find' : 'filter'](function(model) {
952
+ return matches(model.attributes);
727
953
  });
728
954
  },
729
955
 
956
+ // Return the first model with matching attributes. Useful for simple cases
957
+ // of `find`.
958
+ findWhere: function(attrs) {
959
+ return this.where(attrs, true);
960
+ },
961
+
730
962
  // Force the collection to re-sort itself. You don't need to call this under
731
963
  // normal circumstances, as the set will maintain sort order as each item
732
964
  // is added.
733
965
  sort: function(options) {
734
- if (!this.comparator) {
735
- throw new Error('Cannot sort a set without a comparator');
736
- }
966
+ if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
737
967
  options || (options = {});
738
968
 
739
969
  // Run sort based on type of `comparator`.
@@ -752,70 +982,21 @@
752
982
  return _.invoke(this.models, 'get', attr);
753
983
  },
754
984
 
755
- // Smartly update a collection with a change set of models, adding,
756
- // removing, and merging as necessary.
757
- update: function(models, options) {
758
- options = _.extend({add: true, merge: true, remove: true}, options);
759
- if (options.parse) models = this.parse(models, options);
760
- var model, i, l, existing;
761
- var add = [], remove = [], modelMap = {};
762
-
763
- // Allow a single model (or no argument) to be passed.
764
- if (!_.isArray(models)) models = models ? [models] : [];
765
-
766
- // Proxy to `add` for this case, no need to iterate...
767
- if (options.add && !options.remove) return this.add(models, options);
768
-
769
- // Determine which models to add and merge, and which to remove.
770
- for (i = 0, l = models.length; i < l; i++) {
771
- model = models[i];
772
- existing = this.get(model);
773
- if (options.remove && existing) modelMap[existing.cid] = true;
774
- if ((options.add && !existing) || (options.merge && existing)) {
775
- add.push(model);
776
- }
777
- }
778
- if (options.remove) {
779
- for (i = 0, l = this.models.length; i < l; i++) {
780
- model = this.models[i];
781
- if (!modelMap[model.cid]) remove.push(model);
782
- }
783
- }
784
-
785
- // Remove models (if applicable) before we add and merge the rest.
786
- if (remove.length) this.remove(remove, options);
787
- if (add.length) this.add(add, options);
788
- return this;
789
- },
790
-
791
- // When you have more items than you want to add or remove individually,
792
- // you can reset the entire set with a new list of models, without firing
793
- // any `add` or `remove` events. Fires `reset` when finished.
794
- reset: function(models, options) {
795
- options || (options = {});
796
- if (options.parse) models = this.parse(models, options);
797
- for (var i = 0, l = this.models.length; i < l; i++) {
798
- this._removeReference(this.models[i]);
799
- }
800
- options.previousModels = this.models.slice();
801
- this._reset();
802
- if (models) this.add(models, _.extend({silent: true}, options));
803
- if (!options.silent) this.trigger('reset', this, options);
804
- return this;
805
- },
806
-
807
985
  // Fetch the default set of models for this collection, resetting the
808
- // collection when they arrive. If `update: true` is passed, the response
809
- // data will be passed through the `update` method instead of `reset`.
986
+ // collection when they arrive. If `reset: true` is passed, the response
987
+ // data will be passed through the `reset` method instead of `set`.
810
988
  fetch: function(options) {
811
989
  options = options ? _.clone(options) : {};
812
990
  if (options.parse === void 0) options.parse = true;
813
991
  var success = options.success;
814
- options.success = function(collection, resp, options) {
815
- var method = options.update ? 'update' : 'reset';
992
+ var collection = this;
993
+ options.success = function(resp) {
994
+ var method = options.reset ? 'reset' : 'set';
816
995
  collection[method](resp, options);
817
- if (success) success(collection, resp, options);
996
+ if (success) success.call(options.context, collection, resp, options);
997
+ collection.trigger('sync', collection, resp, options);
818
998
  };
999
+ wrapError(this, options);
819
1000
  return this.sync('read', this, options);
820
1001
  },
821
1002
 
@@ -824,13 +1005,14 @@
824
1005
  // wait for the server to agree.
825
1006
  create: function(model, options) {
826
1007
  options = options ? _.clone(options) : {};
1008
+ var wait = options.wait;
827
1009
  if (!(model = this._prepareModel(model, options))) return false;
828
- if (!options.wait) this.add(model, options);
1010
+ if (!wait) this.add(model, options);
829
1011
  var collection = this;
830
1012
  var success = options.success;
831
- options.success = function(model, resp, options) {
832
- if (options.wait) collection.add(model, options);
833
- if (success) success(model, resp, options);
1013
+ options.success = function(model, resp, callbackOpts) {
1014
+ if (wait) collection.add(model, callbackOpts);
1015
+ if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
834
1016
  };
835
1017
  model.save(null, options);
836
1018
  return model;
@@ -844,31 +1026,83 @@
844
1026
 
845
1027
  // Create a new collection with an identical list of models as this one.
846
1028
  clone: function() {
847
- return new this.constructor(this.models);
1029
+ return new this.constructor(this.models, {
1030
+ model: this.model,
1031
+ comparator: this.comparator
1032
+ });
848
1033
  },
849
1034
 
850
- // Reset all internal state. Called when the collection is reset.
1035
+ // Define how to uniquely identify models in the collection.
1036
+ modelId: function (attrs) {
1037
+ return attrs[this.model.prototype.idAttribute || 'id'];
1038
+ },
1039
+
1040
+ // Private method to reset all internal state. Called when the collection
1041
+ // is first initialized or reset.
851
1042
  _reset: function() {
852
1043
  this.length = 0;
853
- this.models.length = 0;
1044
+ this.models = [];
854
1045
  this._byId = {};
855
1046
  },
856
1047
 
857
- // Prepare a model or hash of attributes to be added to this collection.
1048
+ // Prepare a hash of attributes (or other model) to be added to this
1049
+ // collection.
858
1050
  _prepareModel: function(attrs, options) {
859
- if (attrs instanceof Model) {
1051
+ if (this._isModel(attrs)) {
860
1052
  if (!attrs.collection) attrs.collection = this;
861
1053
  return attrs;
862
1054
  }
863
- options || (options = {});
1055
+ options = options ? _.clone(options) : {};
864
1056
  options.collection = this;
865
1057
  var model = new this.model(attrs, options);
866
- if (!model._validate(attrs, options)) return false;
867
- return model;
1058
+ if (!model.validationError) return model;
1059
+ this.trigger('invalid', this, model.validationError, options);
1060
+ return false;
1061
+ },
1062
+
1063
+ // Internal method called by both remove and set. Does not trigger any
1064
+ // additional events. Returns true if anything was actually removed.
1065
+ _removeModels: function(models, options) {
1066
+ var i, l, index, model, removed = false;
1067
+ for (var i = 0, j = 0; i < models.length; i++) {
1068
+ var model = models[i] = this.get(models[i]);
1069
+ if (!model) continue;
1070
+ var id = this.modelId(model.attributes);
1071
+ if (id != null) delete this._byId[id];
1072
+ delete this._byId[model.cid];
1073
+ var index = this.indexOf(model);
1074
+ this.models.splice(index, 1);
1075
+ this.length--;
1076
+ if (!options.silent) {
1077
+ options.index = index;
1078
+ model.trigger('remove', model, this, options);
1079
+ }
1080
+ models[j++] = model;
1081
+ this._removeReference(model, options);
1082
+ removed = true;
1083
+ }
1084
+ // We only need to slice if models array should be smaller, which is
1085
+ // caused by some models not actually getting removed.
1086
+ if (models.length !== j) models = models.slice(0, j);
1087
+ return removed;
1088
+ },
1089
+
1090
+ // Method for checking whether an object should be considered a model for
1091
+ // the purposes of adding to the collection.
1092
+ _isModel: function (model) {
1093
+ return model instanceof Model;
1094
+ },
1095
+
1096
+ // Internal method to create a model's ties to a collection.
1097
+ _addReference: function(model, options) {
1098
+ this._byId[model.cid] = model;
1099
+ var id = this.modelId(model.attributes);
1100
+ if (id != null) this._byId[id] = model;
1101
+ model.on('all', this._onModelEvent, this);
868
1102
  },
869
1103
 
870
- // Internal method to remove a model's ties to a collection.
871
- _removeReference: function(model) {
1104
+ // Internal method to sever a model's ties to a collection.
1105
+ _removeReference: function(model, options) {
872
1106
  if (this === model.collection) delete model.collection;
873
1107
  model.off('all', this._onModelEvent, this);
874
1108
  },
@@ -880,45 +1114,39 @@
880
1114
  _onModelEvent: function(event, model, collection, options) {
881
1115
  if ((event === 'add' || event === 'remove') && collection !== this) return;
882
1116
  if (event === 'destroy') this.remove(model, options);
883
- if (model && event === 'change:' + model.idAttribute) {
884
- delete this._byId[model.previous(model.idAttribute)];
885
- if (model.id != null) this._byId[model.id] = model;
1117
+ if (event === 'change') {
1118
+ var prevId = this.modelId(model.previousAttributes());
1119
+ var id = this.modelId(model.attributes);
1120
+ if (prevId !== id) {
1121
+ if (prevId != null) delete this._byId[prevId];
1122
+ if (id != null) this._byId[id] = model;
1123
+ }
886
1124
  }
887
1125
  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);
896
1126
  }
897
1127
 
898
1128
  });
899
1129
 
900
1130
  // Underscore methods that we want to implement on the Collection.
901
- var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
902
- 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
903
- 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
904
- 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
905
- 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
906
- 'isEmpty', 'chain'];
1131
+ // 90% of the core usefulness of Backbone Collections is actually implemented
1132
+ // right here:
1133
+ var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4,
1134
+ foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3,
1135
+ select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 2,
1136
+ contains: 2, invoke: 2, max: 3, min: 3, toArray: 1, size: 1, first: 3,
1137
+ head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
1138
+ without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
1139
+ isEmpty: 1, chain: 1, sample: 3, partition: 3 };
907
1140
 
908
1141
  // Mix in each Underscore method as a proxy to `Collection#models`.
909
- _.each(methods, function(method) {
910
- Collection.prototype[method] = function() {
911
- var args = slice.call(arguments);
912
- args.unshift(this.models);
913
- return _[method].apply(_, args);
914
- };
915
- });
1142
+ addUnderscoreMethods(Collection, collectionMethods, 'models');
916
1143
 
917
1144
  // Underscore methods that take a property name as an argument.
918
- var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
1145
+ var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
919
1146
 
920
1147
  // Use attributes instead of properties.
921
1148
  _.each(attributeMethods, function(method) {
1149
+ if (!_[method]) return;
922
1150
  Collection.prototype[method] = function(value, context) {
923
1151
  var iterator = _.isFunction(value) ? value : function(model) {
924
1152
  return model.get(value);
@@ -927,27 +1155,281 @@
927
1155
  };
928
1156
  });
929
1157
 
930
- // Backbone.Router
931
- // ---------------
1158
+ // Backbone.View
1159
+ // -------------
932
1160
 
933
- // Routers map faux-URLs to actions, and fire events when routes are
934
- // matched. Creating a new one sets its `routes` hash, if not set statically.
935
- var Router = Backbone.Router = function(options) {
1161
+ // Backbone Views are almost more convention than they are actual code. A View
1162
+ // is simply a JavaScript object that represents a logical chunk of UI in the
1163
+ // DOM. This might be a single item, an entire list, a sidebar or panel, or
1164
+ // even the surrounding frame which wraps your whole app. Defining a chunk of
1165
+ // UI as a **View** allows you to define your DOM events declaratively, without
1166
+ // having to worry about render order ... and makes it easy for the view to
1167
+ // react to specific changes in the state of your models.
1168
+
1169
+ // Creating a Backbone.View creates its initial element outside of the DOM,
1170
+ // if an existing element is not provided...
1171
+ var View = Backbone.View = function(options) {
1172
+ this.cid = _.uniqueId('view');
936
1173
  options || (options = {});
937
- if (options.routes) this.routes = options.routes;
938
- this._bindRoutes();
1174
+ _.extend(this, _.pick(options, viewOptions));
1175
+ this._ensureElement();
939
1176
  this.initialize.apply(this, arguments);
940
1177
  };
941
1178
 
942
- // Cached regular expressions for matching named param parts and splatted
943
- // parts of route strings.
944
- var optionalParam = /\((.*?)\)/g;
945
- var namedParam = /(\(\?)?:\w+/g;
946
- var splatParam = /\*\w+/g;
947
- var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
1179
+ // Cached regex to split keys for `delegate`.
1180
+ var delegateEventSplitter = /^(\S+)\s*(.*)$/;
948
1181
 
949
- // Set up all inheritable **Backbone.Router** properties and methods.
950
- _.extend(Router.prototype, Events, {
1182
+ // List of view options to be merged as properties.
1183
+ var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
1184
+
1185
+ // Set up all inheritable **Backbone.View** properties and methods.
1186
+ _.extend(View.prototype, Events, {
1187
+
1188
+ // The default `tagName` of a View's element is `"div"`.
1189
+ tagName: 'div',
1190
+
1191
+ // jQuery delegate for element lookup, scoped to DOM elements within the
1192
+ // current view. This should be preferred to global lookups where possible.
1193
+ $: function(selector) {
1194
+ return this.$el.find(selector);
1195
+ },
1196
+
1197
+ // Initialize is an empty function by default. Override it with your own
1198
+ // initialization logic.
1199
+ initialize: function(){},
1200
+
1201
+ // **render** is the core function that your view should override, in order
1202
+ // to populate its element (`this.el`), with the appropriate HTML. The
1203
+ // convention is for **render** to always return `this`.
1204
+ render: function() {
1205
+ return this;
1206
+ },
1207
+
1208
+ // Remove this view by taking the element out of the DOM, and removing any
1209
+ // applicable Backbone.Events listeners.
1210
+ remove: function() {
1211
+ this._removeElement();
1212
+ this.stopListening();
1213
+ return this;
1214
+ },
1215
+
1216
+ // Remove this view's element from the document and all event listeners
1217
+ // attached to it. Exposed for subclasses using an alternative DOM
1218
+ // manipulation API.
1219
+ _removeElement: function() {
1220
+ this.$el.remove();
1221
+ },
1222
+
1223
+ // Change the view's element (`this.el` property) and re-delegate the
1224
+ // view's events on the new element.
1225
+ setElement: function(element) {
1226
+ this.undelegateEvents();
1227
+ this._setElement(element);
1228
+ this.delegateEvents();
1229
+ return this;
1230
+ },
1231
+
1232
+ // Creates the `this.el` and `this.$el` references for this view using the
1233
+ // given `el`. `el` can be a CSS selector or an HTML string, a jQuery
1234
+ // context or an element. Subclasses can override this to utilize an
1235
+ // alternative DOM manipulation API and are only required to set the
1236
+ // `this.el` property.
1237
+ _setElement: function(el) {
1238
+ this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
1239
+ this.el = this.$el[0];
1240
+ },
1241
+
1242
+ // Set callbacks, where `this.events` is a hash of
1243
+ //
1244
+ // *{"event selector": "callback"}*
1245
+ //
1246
+ // {
1247
+ // 'mousedown .title': 'edit',
1248
+ // 'click .button': 'save',
1249
+ // 'click .open': function(e) { ... }
1250
+ // }
1251
+ //
1252
+ // pairs. Callbacks will be bound to the view, with `this` set properly.
1253
+ // Uses event delegation for efficiency.
1254
+ // Omitting the selector binds the event to `this.el`.
1255
+ delegateEvents: function(events) {
1256
+ if (!(events || (events = _.result(this, 'events')))) return this;
1257
+ this.undelegateEvents();
1258
+ for (var key in events) {
1259
+ var method = events[key];
1260
+ if (!_.isFunction(method)) method = this[events[key]];
1261
+ if (!method) continue;
1262
+ var match = key.match(delegateEventSplitter);
1263
+ this.delegate(match[1], match[2], _.bind(method, this));
1264
+ }
1265
+ return this;
1266
+ },
1267
+
1268
+ // Add a single event listener to the view's element (or a child element
1269
+ // using `selector`). This only works for delegate-able events: not `focus`,
1270
+ // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
1271
+ delegate: function(eventName, selector, listener) {
1272
+ this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
1273
+ },
1274
+
1275
+ // Clears all callbacks previously bound to the view by `delegateEvents`.
1276
+ // You usually don't need to use this, but may wish to if you have multiple
1277
+ // Backbone views attached to the same DOM element.
1278
+ undelegateEvents: function() {
1279
+ if (this.$el) this.$el.off('.delegateEvents' + this.cid);
1280
+ return this;
1281
+ },
1282
+
1283
+ // A finer-grained `undelegateEvents` for removing a single delegated event.
1284
+ // `selector` and `listener` are both optional.
1285
+ undelegate: function(eventName, selector, listener) {
1286
+ this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
1287
+ },
1288
+
1289
+ // Produces a DOM element to be assigned to your view. Exposed for
1290
+ // subclasses using an alternative DOM manipulation API.
1291
+ _createElement: function(tagName) {
1292
+ return document.createElement(tagName);
1293
+ },
1294
+
1295
+ // Ensure that the View has a DOM element to render into.
1296
+ // If `this.el` is a string, pass it through `$()`, take the first
1297
+ // matching element, and re-assign it to `el`. Otherwise, create
1298
+ // an element from the `id`, `className` and `tagName` properties.
1299
+ _ensureElement: function() {
1300
+ if (!this.el) {
1301
+ var attrs = _.extend({}, _.result(this, 'attributes'));
1302
+ if (this.id) attrs.id = _.result(this, 'id');
1303
+ if (this.className) attrs['class'] = _.result(this, 'className');
1304
+ this.setElement(this._createElement(_.result(this, 'tagName')));
1305
+ this._setAttributes(attrs);
1306
+ } else {
1307
+ this.setElement(_.result(this, 'el'));
1308
+ }
1309
+ },
1310
+
1311
+ // Set attributes from a hash on this view's element. Exposed for
1312
+ // subclasses using an alternative DOM manipulation API.
1313
+ _setAttributes: function(attributes) {
1314
+ this.$el.attr(attributes);
1315
+ }
1316
+
1317
+ });
1318
+
1319
+ // Backbone.sync
1320
+ // -------------
1321
+
1322
+ // Override this function to change the manner in which Backbone persists
1323
+ // models to the server. You will be passed the type of request, and the
1324
+ // model in question. By default, makes a RESTful Ajax request
1325
+ // to the model's `url()`. Some possible customizations could be:
1326
+ //
1327
+ // * Use `setTimeout` to batch rapid-fire updates into a single request.
1328
+ // * Send up the models as XML instead of JSON.
1329
+ // * Persist models via WebSockets instead of Ajax.
1330
+ //
1331
+ // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1332
+ // as `POST`, with a `_method` parameter containing the true HTTP method,
1333
+ // as well as all requests with the body as `application/x-www-form-urlencoded`
1334
+ // instead of `application/json` with the model in a param named `model`.
1335
+ // Useful when interfacing with server-side languages like **PHP** that make
1336
+ // it difficult to read the body of `PUT` requests.
1337
+ Backbone.sync = function(method, model, options) {
1338
+ var type = methodMap[method];
1339
+
1340
+ // Default options, unless specified.
1341
+ _.defaults(options || (options = {}), {
1342
+ emulateHTTP: Backbone.emulateHTTP,
1343
+ emulateJSON: Backbone.emulateJSON
1344
+ });
1345
+
1346
+ // Default JSON-request options.
1347
+ var params = {type: type, dataType: 'json'};
1348
+
1349
+ // Ensure that we have a URL.
1350
+ if (!options.url) {
1351
+ params.url = _.result(model, 'url') || urlError();
1352
+ }
1353
+
1354
+ // Ensure that we have the appropriate request data.
1355
+ if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
1356
+ params.contentType = 'application/json';
1357
+ params.data = JSON.stringify(options.attrs || model.toJSON(options));
1358
+ }
1359
+
1360
+ // For older servers, emulate JSON by encoding the request into an HTML-form.
1361
+ if (options.emulateJSON) {
1362
+ params.contentType = 'application/x-www-form-urlencoded';
1363
+ params.data = params.data ? {model: params.data} : {};
1364
+ }
1365
+
1366
+ // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1367
+ // And an `X-HTTP-Method-Override` header.
1368
+ if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
1369
+ params.type = 'POST';
1370
+ if (options.emulateJSON) params.data._method = type;
1371
+ var beforeSend = options.beforeSend;
1372
+ options.beforeSend = function(xhr) {
1373
+ xhr.setRequestHeader('X-HTTP-Method-Override', type);
1374
+ if (beforeSend) return beforeSend.apply(this, arguments);
1375
+ };
1376
+ }
1377
+
1378
+ // Don't process data on a non-GET request.
1379
+ if (params.type !== 'GET' && !options.emulateJSON) {
1380
+ params.processData = false;
1381
+ }
1382
+
1383
+ // Pass along `textStatus` and `errorThrown` from jQuery.
1384
+ var error = options.error;
1385
+ options.error = function(xhr, textStatus, errorThrown) {
1386
+ options.textStatus = textStatus;
1387
+ options.errorThrown = errorThrown;
1388
+ if (error) error.call(options.context, xhr, textStatus, errorThrown);
1389
+ };
1390
+
1391
+ // Make the request, allowing the user to override any Ajax options.
1392
+ var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
1393
+ model.trigger('request', model, xhr, options);
1394
+ return xhr;
1395
+ };
1396
+
1397
+ // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1398
+ var methodMap = {
1399
+ 'create': 'POST',
1400
+ 'update': 'PUT',
1401
+ 'patch': 'PATCH',
1402
+ 'delete': 'DELETE',
1403
+ 'read': 'GET'
1404
+ };
1405
+
1406
+ // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
1407
+ // Override this if you'd like to use a different library.
1408
+ Backbone.ajax = function() {
1409
+ return Backbone.$.ajax.apply(Backbone.$, arguments);
1410
+ };
1411
+
1412
+ // Backbone.Router
1413
+ // ---------------
1414
+
1415
+ // Routers map faux-URLs to actions, and fire events when routes are
1416
+ // matched. Creating a new one sets its `routes` hash, if not set statically.
1417
+ var Router = Backbone.Router = function(options) {
1418
+ options || (options = {});
1419
+ if (options.routes) this.routes = options.routes;
1420
+ this._bindRoutes();
1421
+ this.initialize.apply(this, arguments);
1422
+ };
1423
+
1424
+ // Cached regular expressions for matching named param parts and splatted
1425
+ // parts of route strings.
1426
+ var optionalParam = /\((.*?)\)/g;
1427
+ var namedParam = /(\(\?)?:\w+/g;
1428
+ var splatParam = /\*\w+/g;
1429
+ var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
1430
+
1431
+ // Set up all inheritable **Backbone.Router** properties and methods.
1432
+ _.extend(Router.prototype, Events, {
951
1433
 
952
1434
  // Initialize is an empty function by default. Override it with your own
953
1435
  // initialization logic.
@@ -961,17 +1443,29 @@
961
1443
  //
962
1444
  route: function(route, name, callback) {
963
1445
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
1446
+ if (_.isFunction(name)) {
1447
+ callback = name;
1448
+ name = '';
1449
+ }
964
1450
  if (!callback) callback = this[name];
965
- Backbone.history.route(route, _.bind(function(fragment) {
966
- var args = this._extractParameters(route, fragment);
967
- callback && callback.apply(this, args);
968
- this.trigger.apply(this, ['route:' + name].concat(args));
969
- this.trigger('route', name, args);
970
- Backbone.history.trigger('route', this, name, args);
971
- }, this));
1451
+ var router = this;
1452
+ Backbone.history.route(route, function(fragment) {
1453
+ var args = router._extractParameters(route, fragment);
1454
+ if (router.execute(callback, args, name) !== false) {
1455
+ router.trigger.apply(router, ['route:' + name].concat(args));
1456
+ router.trigger('route', name, args);
1457
+ Backbone.history.trigger('route', router, name, args);
1458
+ }
1459
+ });
972
1460
  return this;
973
1461
  },
974
1462
 
1463
+ // Execute a route handler with the provided parameters. This is an
1464
+ // excellent place to do pre-route setup or post-route cleanup.
1465
+ execute: function(callback, args, name) {
1466
+ if (callback) callback.apply(this, args);
1467
+ },
1468
+
975
1469
  // Simple proxy to `Backbone.history` to save a fragment into the history.
976
1470
  navigate: function(fragment, options) {
977
1471
  Backbone.history.navigate(fragment, options);
@@ -983,6 +1477,7 @@
983
1477
  // routes can be defined at the bottom of the route map.
984
1478
  _bindRoutes: function() {
985
1479
  if (!this.routes) return;
1480
+ this.routes = _.result(this, 'routes');
986
1481
  var route, routes = _.keys(this.routes);
987
1482
  while ((route = routes.pop()) != null) {
988
1483
  this.route(route, this.routes[route]);
@@ -994,17 +1489,23 @@
994
1489
  _routeToRegExp: function(route) {
995
1490
  route = route.replace(escapeRegExp, '\\$&')
996
1491
  .replace(optionalParam, '(?:$1)?')
997
- .replace(namedParam, function(match, optional){
998
- return optional ? match : '([^\/]+)';
1492
+ .replace(namedParam, function(match, optional) {
1493
+ return optional ? match : '([^/?]+)';
999
1494
  })
1000
- .replace(splatParam, '(.*?)');
1001
- return new RegExp('^' + route + '$');
1495
+ .replace(splatParam, '([^?]*?)');
1496
+ return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
1002
1497
  },
1003
1498
 
1004
1499
  // Given a route, and a URL fragment that it matches, return the array of
1005
- // extracted parameters.
1500
+ // extracted decoded parameters. Empty or unmatched parameters will be
1501
+ // treated as `null` to normalize cross-browser behavior.
1006
1502
  _extractParameters: function(route, fragment) {
1007
- return route.exec(fragment).slice(1);
1503
+ var params = route.exec(fragment).slice(1);
1504
+ return _.map(params, function(param, i) {
1505
+ // Don't decode the search params.
1506
+ if (i === params.length - 1) return param || null;
1507
+ return param ? decodeURIComponent(param) : null;
1508
+ });
1008
1509
  }
1009
1510
 
1010
1511
  });
@@ -1012,8 +1513,11 @@
1012
1513
  // Backbone.History
1013
1514
  // ----------------
1014
1515
 
1015
- // Handles cross-browser history management, based on URL fragments. If the
1016
- // browser does not support `onhashchange`, falls back to polling.
1516
+ // Handles cross-browser history management, based on either
1517
+ // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
1518
+ // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
1519
+ // and URL fragments. If the browser supports neither (old IE, natch),
1520
+ // falls back to polling.
1017
1521
  var History = Backbone.History = function() {
1018
1522
  this.handlers = [];
1019
1523
  _.bindAll(this, 'checkUrl');
@@ -1031,11 +1535,8 @@
1031
1535
  // Cached regex for stripping leading and trailing slashes.
1032
1536
  var rootStripper = /^\/+|\/+$/g;
1033
1537
 
1034
- // Cached regex for detecting MSIE.
1035
- var isExplorer = /msie [\w.]+/;
1036
-
1037
- // Cached regex for removing a trailing slash.
1038
- var trailingSlash = /\/$/;
1538
+ // Cached regex for stripping urls of hash.
1539
+ var pathStripper = /#.*$/;
1039
1540
 
1040
1541
  // Has the history handling already been started?
1041
1542
  History.started = false;
@@ -1047,6 +1548,33 @@
1047
1548
  // twenty times a second.
1048
1549
  interval: 50,
1049
1550
 
1551
+ // Are we at the app root?
1552
+ atRoot: function() {
1553
+ var path = this.location.pathname.replace(/[^\/]$/, '$&/');
1554
+ return path === this.root && !this.getSearch();
1555
+ },
1556
+
1557
+ // Does the pathname match the root?
1558
+ matchRoot: function() {
1559
+ var path = this.decodeFragment(this.location.pathname);
1560
+ var root = path.slice(0, this.root.length - 1) + '/';
1561
+ return root === this.root;
1562
+ },
1563
+
1564
+ // Unicode characters in `location.pathname` are percent encoded so they're
1565
+ // decoded for comparison. `%25` should not be decoded since it may be part
1566
+ // of an encoded parameter.
1567
+ decodeFragment: function(fragment) {
1568
+ return decodeURI(fragment.replace(/%25/g, '%2525'));
1569
+ },
1570
+
1571
+ // In IE6, the hash fragment and search params are incorrect if the
1572
+ // fragment contains `?`.
1573
+ getSearch: function() {
1574
+ var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
1575
+ return match ? match[0] : '';
1576
+ },
1577
+
1050
1578
  // Gets the true hash value. Cannot use location.hash directly due to bug
1051
1579
  // in Firefox where location.hash will always be decoded.
1052
1580
  getHash: function(window) {
@@ -1054,14 +1582,19 @@
1054
1582
  return match ? match[1] : '';
1055
1583
  },
1056
1584
 
1057
- // Get the cross-browser normalized URL fragment, either from the URL,
1058
- // the hash, or the override.
1059
- getFragment: function(fragment, forcePushState) {
1585
+ // Get the pathname and search params, without the root.
1586
+ getPath: function() {
1587
+ var path = this.decodeFragment(
1588
+ this.location.pathname + this.getSearch()
1589
+ ).slice(this.root.length - 1);
1590
+ return path.charAt(0) === '/' ? path.slice(1) : path;
1591
+ },
1592
+
1593
+ // Get the cross-browser normalized URL fragment from the path or hash.
1594
+ getFragment: function(fragment) {
1060
1595
  if (fragment == null) {
1061
- if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1062
- fragment = this.location.pathname;
1063
- var root = this.root.replace(trailingSlash, '');
1064
- if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
1596
+ if (this._usePushState || !this._wantsHashChange) {
1597
+ fragment = this.getPath();
1065
1598
  } else {
1066
1599
  fragment = this.getHash();
1067
1600
  }
@@ -1072,67 +1605,100 @@
1072
1605
  // Start the hash change handling, returning `true` if the current URL matches
1073
1606
  // an existing route, and `false` otherwise.
1074
1607
  start: function(options) {
1075
- if (History.started) throw new Error("Backbone.history has already been started");
1608
+ if (History.started) throw new Error('Backbone.history has already been started');
1076
1609
  History.started = true;
1077
1610
 
1078
1611
  // Figure out the initial configuration. Do we need an iframe?
1079
1612
  // Is pushState desired ... is it available?
1080
- this.options = _.extend({}, {root: '/'}, this.options, options);
1613
+ this.options = _.extend({root: '/'}, this.options, options);
1081
1614
  this.root = this.options.root;
1082
1615
  this._wantsHashChange = this.options.hashChange !== false;
1616
+ this._hasHashChange = 'onhashchange' in window;
1617
+ this._useHashChange = this._wantsHashChange && this._hasHashChange;
1083
1618
  this._wantsPushState = !!this.options.pushState;
1084
- this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
1085
- var fragment = this.getFragment();
1086
- var docMode = document.documentMode;
1087
- var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1619
+ this._hasPushState = !!(this.history && this.history.pushState);
1620
+ this._usePushState = this._wantsPushState && this._hasPushState;
1621
+ this.fragment = this.getFragment();
1088
1622
 
1089
1623
  // Normalize root to always include a leading and trailing slash.
1090
1624
  this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1091
1625
 
1092
- if (oldIE && this._wantsHashChange) {
1093
- this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
1094
- this.navigate(fragment);
1626
+ // Transition from hashChange to pushState or vice versa if both are
1627
+ // requested.
1628
+ if (this._wantsHashChange && this._wantsPushState) {
1629
+
1630
+ // If we've started off with a route from a `pushState`-enabled
1631
+ // browser, but we're currently in a browser that doesn't support it...
1632
+ if (!this._hasPushState && !this.atRoot()) {
1633
+ var root = this.root.slice(0, -1) || '/';
1634
+ this.location.replace(root + '#' + this.getPath());
1635
+ // Return immediately as browser will do redirect to new url
1636
+ return true;
1637
+
1638
+ // Or if we've started out with a hash-based route, but we're currently
1639
+ // in a browser where it could be `pushState`-based instead...
1640
+ } else if (this._hasPushState && this.atRoot()) {
1641
+ this.navigate(this.getHash(), {replace: true});
1642
+ }
1643
+
1644
+ }
1645
+
1646
+ // Proxy an iframe to handle location events if the browser doesn't
1647
+ // support the `hashchange` event, HTML5 history, or the user wants
1648
+ // `hashChange` but not `pushState`.
1649
+ if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
1650
+ var iframe = document.createElement('iframe');
1651
+ iframe.src = 'javascript:0';
1652
+ iframe.style.display = 'none';
1653
+ iframe.tabIndex = -1;
1654
+ var body = document.body;
1655
+ // Using `appendChild` will throw on IE < 9 if the document is not ready.
1656
+ this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow;
1657
+ this.iframe.document.open().close();
1658
+ this.iframe.location.hash = '#' + this.fragment;
1095
1659
  }
1096
1660
 
1661
+ // Add a cross-platform `addEventListener` shim for older browsers.
1662
+ var addEventListener = window.addEventListener || function (eventName, listener) {
1663
+ return attachEvent('on' + eventName, listener);
1664
+ };
1665
+
1097
1666
  // Depending on whether we're using pushState or hashes, and whether
1098
1667
  // 'onhashchange' is supported, determine how we check the URL state.
1099
- if (this._hasPushState) {
1100
- Backbone.$(window).on('popstate', this.checkUrl);
1101
- } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1102
- Backbone.$(window).on('hashchange', this.checkUrl);
1668
+ if (this._usePushState) {
1669
+ addEventListener('popstate', this.checkUrl, false);
1670
+ } else if (this._useHashChange && !this.iframe) {
1671
+ addEventListener('hashchange', this.checkUrl, false);
1103
1672
  } else if (this._wantsHashChange) {
1104
1673
  this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1105
1674
  }
1106
1675
 
1107
- // Determine if we need to change the base url, for a pushState link
1108
- // opened by a non-pushState browser.
1109
- this.fragment = fragment;
1110
- var loc = this.location;
1111
- var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1112
-
1113
- // If we've started off with a route from a `pushState`-enabled browser,
1114
- // but we're currently in a browser that doesn't support it...
1115
- if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1116
- this.fragment = this.getFragment(null, true);
1117
- this.location.replace(this.root + this.location.search + '#' + this.fragment);
1118
- // Return immediately as browser will do redirect to new url
1119
- return true;
1120
-
1121
- // Or if we've started out with a hash-based route, but we're currently
1122
- // in a browser where it could be `pushState`-based instead...
1123
- } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1124
- this.fragment = this.getHash().replace(routeStripper, '');
1125
- this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1126
- }
1127
-
1128
1676
  if (!this.options.silent) return this.loadUrl();
1129
1677
  },
1130
1678
 
1131
1679
  // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1132
1680
  // but possibly useful for unit testing Routers.
1133
1681
  stop: function() {
1134
- Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1135
- clearInterval(this._checkUrlInterval);
1682
+ // Add a cross-platform `removeEventListener` shim for older browsers.
1683
+ var removeEventListener = window.removeEventListener || function (eventName, listener) {
1684
+ return detachEvent('on' + eventName, listener);
1685
+ };
1686
+
1687
+ // Remove window listeners.
1688
+ if (this._usePushState) {
1689
+ removeEventListener('popstate', this.checkUrl, false);
1690
+ } else if (this._useHashChange && !this.iframe) {
1691
+ removeEventListener('hashchange', this.checkUrl, false);
1692
+ }
1693
+
1694
+ // Clean up the iframe if necessary.
1695
+ if (this.iframe) {
1696
+ document.body.removeChild(this.iframe.frameElement);
1697
+ this.iframe = null;
1698
+ }
1699
+
1700
+ // Some environments will throw when clearing an undefined interval.
1701
+ if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
1136
1702
  History.started = false;
1137
1703
  },
1138
1704
 
@@ -1146,26 +1712,31 @@
1146
1712
  // calls `loadUrl`, normalizing across the hidden iframe.
1147
1713
  checkUrl: function(e) {
1148
1714
  var current = this.getFragment();
1715
+
1716
+ // If the user pressed the back button, the iframe's hash will have
1717
+ // changed and we should use that for comparison.
1149
1718
  if (current === this.fragment && this.iframe) {
1150
- current = this.getFragment(this.getHash(this.iframe));
1719
+ current = this.getHash(this.iframe);
1151
1720
  }
1721
+
1152
1722
  if (current === this.fragment) return false;
1153
1723
  if (this.iframe) this.navigate(current);
1154
- this.loadUrl() || this.loadUrl(this.getHash());
1724
+ this.loadUrl();
1155
1725
  },
1156
1726
 
1157
1727
  // Attempt to load the current URL fragment. If a route succeeds with a
1158
1728
  // match, returns `true`. If no defined routes matches the fragment,
1159
1729
  // returns `false`.
1160
- loadUrl: function(fragmentOverride) {
1161
- var fragment = this.fragment = this.getFragment(fragmentOverride);
1162
- var matched = _.any(this.handlers, function(handler) {
1730
+ loadUrl: function(fragment) {
1731
+ // If the root doesn't match, no routes can match either.
1732
+ if (!this.matchRoot()) return false;
1733
+ fragment = this.fragment = this.getFragment(fragment);
1734
+ return _.any(this.handlers, function(handler) {
1163
1735
  if (handler.route.test(fragment)) {
1164
1736
  handler.callback(fragment);
1165
1737
  return true;
1166
1738
  }
1167
1739
  });
1168
- return matched;
1169
1740
  },
1170
1741
 
1171
1742
  // Save a fragment into the hash history, or replace the URL state if the
@@ -1177,25 +1748,37 @@
1177
1748
  // you wish to modify the current URL without adding an entry to the history.
1178
1749
  navigate: function(fragment, options) {
1179
1750
  if (!History.started) return false;
1180
- if (!options || options === true) options = {trigger: options};
1751
+ if (!options || options === true) options = {trigger: !!options};
1752
+
1753
+ // Normalize the fragment.
1181
1754
  fragment = this.getFragment(fragment || '');
1755
+
1756
+ // Don't include a trailing slash on the root.
1757
+ var root = this.root;
1758
+ if (fragment === '' || fragment.charAt(0) === '?') {
1759
+ root = root.slice(0, -1) || '/';
1760
+ }
1761
+ var url = root + fragment;
1762
+
1763
+ // Strip the hash and decode for matching.
1764
+ fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
1765
+
1182
1766
  if (this.fragment === fragment) return;
1183
1767
  this.fragment = fragment;
1184
- var url = this.root + fragment;
1185
1768
 
1186
1769
  // If pushState is available, we use it to set the fragment as a real URL.
1187
- if (this._hasPushState) {
1770
+ if (this._usePushState) {
1188
1771
  this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1189
1772
 
1190
1773
  // If hash changes haven't been explicitly disabled, update the hash
1191
1774
  // fragment to store history.
1192
1775
  } else if (this._wantsHashChange) {
1193
1776
  this._updateHash(this.location, fragment, options.replace);
1194
- if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
1777
+ if (this.iframe && (fragment !== this.getHash(this.iframe))) {
1195
1778
  // Opening and closing the iframe tricks IE7 and earlier to push a
1196
1779
  // history entry on hash-tag change. When replace is true, we don't
1197
1780
  // want this.
1198
- if(!options.replace) this.iframe.document.open().close();
1781
+ if (!options.replace) this.iframe.document.open().close();
1199
1782
  this._updateHash(this.iframe.location, fragment, options.replace);
1200
1783
  }
1201
1784
 
@@ -1204,7 +1787,7 @@
1204
1787
  } else {
1205
1788
  return this.location.assign(url);
1206
1789
  }
1207
- if (options.trigger) this.loadUrl(fragment);
1790
+ if (options.trigger) return this.loadUrl(fragment);
1208
1791
  },
1209
1792
 
1210
1793
  // Update the hash location, either replacing the current entry, or adding
@@ -1224,234 +1807,10 @@
1224
1807
  // Create the default Backbone.history.
1225
1808
  Backbone.history = new History;
1226
1809
 
1227
- // Backbone.View
1228
- // -------------
1229
-
1230
- // Creating a Backbone.View creates its initial element outside of the DOM,
1231
- // if an existing element is not provided...
1232
- var View = Backbone.View = function(options) {
1233
- this.cid = _.uniqueId('view');
1234
- this._configure(options || {});
1235
- this._ensureElement();
1236
- this.initialize.apply(this, arguments);
1237
- this.delegateEvents();
1238
- };
1239
-
1240
- // Cached regex to split keys for `delegate`.
1241
- var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1242
-
1243
- // List of view options to be merged as properties.
1244
- var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
1245
-
1246
- // Set up all inheritable **Backbone.View** properties and methods.
1247
- _.extend(View.prototype, Events, {
1248
-
1249
- // The default `tagName` of a View's element is `"div"`.
1250
- tagName: 'div',
1251
-
1252
- // jQuery delegate for element lookup, scoped to DOM elements within the
1253
- // current view. This should be prefered to global lookups where possible.
1254
- $: function(selector) {
1255
- return this.$el.find(selector);
1256
- },
1257
-
1258
- // Initialize is an empty function by default. Override it with your own
1259
- // initialization logic.
1260
- initialize: function(){},
1261
-
1262
- // **render** is the core function that your view should override, in order
1263
- // to populate its element (`this.el`), with the appropriate HTML. The
1264
- // convention is for **render** to always return `this`.
1265
- render: function() {
1266
- return this;
1267
- },
1268
-
1269
- // Remove this view by taking the element out of the DOM, and removing any
1270
- // applicable Backbone.Events listeners.
1271
- remove: function() {
1272
- this.$el.remove();
1273
- this.stopListening();
1274
- return this;
1275
- },
1276
-
1277
- // Change the view's element (`this.el` property), including event
1278
- // re-delegation.
1279
- setElement: function(element, delegate) {
1280
- if (this.$el) this.undelegateEvents();
1281
- this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
1282
- this.el = this.$el[0];
1283
- if (delegate !== false) this.delegateEvents();
1284
- return this;
1285
- },
1286
-
1287
- // Set callbacks, where `this.events` is a hash of
1288
- //
1289
- // *{"event selector": "callback"}*
1290
- //
1291
- // {
1292
- // 'mousedown .title': 'edit',
1293
- // 'click .button': 'save'
1294
- // 'click .open': function(e) { ... }
1295
- // }
1296
- //
1297
- // pairs. Callbacks will be bound to the view, with `this` set properly.
1298
- // Uses event delegation for efficiency.
1299
- // Omitting the selector binds the event to `this.el`.
1300
- // This only works for delegate-able events: not `focus`, `blur`, and
1301
- // not `change`, `submit`, and `reset` in Internet Explorer.
1302
- delegateEvents: function(events) {
1303
- if (!(events || (events = _.result(this, 'events')))) return;
1304
- this.undelegateEvents();
1305
- for (var key in events) {
1306
- var method = events[key];
1307
- if (!_.isFunction(method)) method = this[events[key]];
1308
- if (!method) throw new Error('Method "' + events[key] + '" does not exist');
1309
- var match = key.match(delegateEventSplitter);
1310
- var eventName = match[1], selector = match[2];
1311
- method = _.bind(method, this);
1312
- eventName += '.delegateEvents' + this.cid;
1313
- if (selector === '') {
1314
- this.$el.on(eventName, method);
1315
- } else {
1316
- this.$el.on(eventName, selector, method);
1317
- }
1318
- }
1319
- },
1320
-
1321
- // Clears all callbacks previously bound to the view with `delegateEvents`.
1322
- // You usually don't need to use this, but may wish to if you have multiple
1323
- // Backbone views attached to the same DOM element.
1324
- undelegateEvents: function() {
1325
- this.$el.off('.delegateEvents' + this.cid);
1326
- },
1327
-
1328
- // Performs the initial configuration of a View with a set of options.
1329
- // Keys with special meaning *(model, collection, id, className)*, are
1330
- // attached directly to the view.
1331
- _configure: function(options) {
1332
- if (this.options) options = _.extend({}, _.result(this, 'options'), options);
1333
- _.extend(this, _.pick(options, viewOptions));
1334
- this.options = options;
1335
- },
1336
-
1337
- // Ensure that the View has a DOM element to render into.
1338
- // If `this.el` is a string, pass it through `$()`, take the first
1339
- // matching element, and re-assign it to `el`. Otherwise, create
1340
- // an element from the `id`, `className` and `tagName` properties.
1341
- _ensureElement: function() {
1342
- if (!this.el) {
1343
- var attrs = _.extend({}, _.result(this, 'attributes'));
1344
- if (this.id) attrs.id = _.result(this, 'id');
1345
- if (this.className) attrs['class'] = _.result(this, 'className');
1346
- var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
1347
- this.setElement($el, false);
1348
- } else {
1349
- this.setElement(_.result(this, 'el'), false);
1350
- }
1351
- }
1352
-
1353
- });
1354
-
1355
- // Backbone.sync
1356
- // -------------
1357
-
1358
- // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1359
- var methodMap = {
1360
- 'create': 'POST',
1361
- 'update': 'PUT',
1362
- 'patch': 'PATCH',
1363
- 'delete': 'DELETE',
1364
- 'read': 'GET'
1365
- };
1366
-
1367
- // Override this function to change the manner in which Backbone persists
1368
- // models to the server. You will be passed the type of request, and the
1369
- // model in question. By default, makes a RESTful Ajax request
1370
- // to the model's `url()`. Some possible customizations could be:
1371
- //
1372
- // * Use `setTimeout` to batch rapid-fire updates into a single request.
1373
- // * Send up the models as XML instead of JSON.
1374
- // * Persist models via WebSockets instead of Ajax.
1375
- //
1376
- // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1377
- // as `POST`, with a `_method` parameter containing the true HTTP method,
1378
- // as well as all requests with the body as `application/x-www-form-urlencoded`
1379
- // instead of `application/json` with the model in a param named `model`.
1380
- // Useful when interfacing with server-side languages like **PHP** that make
1381
- // it difficult to read the body of `PUT` requests.
1382
- Backbone.sync = function(method, model, options) {
1383
- var type = methodMap[method];
1384
-
1385
- // Default options, unless specified.
1386
- _.defaults(options || (options = {}), {
1387
- emulateHTTP: Backbone.emulateHTTP,
1388
- emulateJSON: Backbone.emulateJSON
1389
- });
1390
-
1391
- // Default JSON-request options.
1392
- var params = {type: type, dataType: 'json'};
1393
-
1394
- // Ensure that we have a URL.
1395
- if (!options.url) {
1396
- params.url = _.result(model, 'url') || urlError();
1397
- }
1398
-
1399
- // Ensure that we have the appropriate request data.
1400
- if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
1401
- params.contentType = 'application/json';
1402
- params.data = JSON.stringify(options.attrs || model.toJSON(options));
1403
- }
1404
-
1405
- // For older servers, emulate JSON by encoding the request into an HTML-form.
1406
- if (options.emulateJSON) {
1407
- params.contentType = 'application/x-www-form-urlencoded';
1408
- params.data = params.data ? {model: params.data} : {};
1409
- }
1410
-
1411
- // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1412
- // And an `X-HTTP-Method-Override` header.
1413
- if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
1414
- params.type = 'POST';
1415
- if (options.emulateJSON) params.data._method = type;
1416
- var beforeSend = options.beforeSend;
1417
- options.beforeSend = function(xhr) {
1418
- xhr.setRequestHeader('X-HTTP-Method-Override', type);
1419
- if (beforeSend) return beforeSend.apply(this, arguments);
1420
- };
1421
- }
1422
-
1423
- // Don't process data on a non-GET request.
1424
- if (params.type !== 'GET' && !options.emulateJSON) {
1425
- params.processData = false;
1426
- }
1427
-
1428
- var success = options.success;
1429
- options.success = function(resp) {
1430
- if (success) success(model, resp, options);
1431
- model.trigger('sync', model, resp, options);
1432
- };
1433
-
1434
- var error = options.error;
1435
- options.error = function(xhr) {
1436
- if (error) error(model, xhr, options);
1437
- model.trigger('error', model, xhr, options);
1438
- };
1439
-
1440
- // Make the request, allowing the user to override any Ajax options.
1441
- var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
1442
- model.trigger('request', model, xhr, options);
1443
- return xhr;
1444
- };
1445
-
1446
- // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
1447
- Backbone.ajax = function() {
1448
- return Backbone.$.ajax.apply(Backbone.$, arguments);
1449
- };
1450
-
1451
1810
  // Helpers
1452
1811
  // -------
1453
1812
 
1454
- // Helper function to correctly set up the prototype chain, for subclasses.
1813
+ // Helper function to correctly set up the prototype chain for subclasses.
1455
1814
  // Similar to `goog.inherits`, but uses a hash of prototype properties and
1456
1815
  // class properties to be extended.
1457
1816
  var extend = function(protoProps, staticProps) {
@@ -1460,7 +1819,7 @@
1460
1819
 
1461
1820
  // The constructor function for the new subclass is either defined by you
1462
1821
  // (the "constructor" property in your `extend` definition), or defaulted
1463
- // by us to simply call the parent's constructor.
1822
+ // by us to simply call the parent constructor.
1464
1823
  if (protoProps && _.has(protoProps, 'constructor')) {
1465
1824
  child = protoProps.constructor;
1466
1825
  } else {
@@ -1471,7 +1830,7 @@
1471
1830
  _.extend(child, parent, staticProps);
1472
1831
 
1473
1832
  // Set the prototype chain to inherit from `parent`, without calling
1474
- // `parent`'s constructor function.
1833
+ // `parent` constructor function.
1475
1834
  var Surrogate = function(){ this.constructor = child; };
1476
1835
  Surrogate.prototype = parent.prototype;
1477
1836
  child.prototype = new Surrogate;
@@ -1495,4 +1854,15 @@
1495
1854
  throw new Error('A "url" property or function must be specified');
1496
1855
  };
1497
1856
 
1498
- }).call(this);
1857
+ // Wrap an optional error callback with a fallback error event.
1858
+ var wrapError = function(model, options) {
1859
+ var error = options.error;
1860
+ options.error = function(resp) {
1861
+ if (error) error.call(options.context, model, resp, options);
1862
+ model.trigger('error', model, resp, options);
1863
+ };
1864
+ };
1865
+
1866
+ return Backbone;
1867
+
1868
+ }));