rails-backbone 0.7.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -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
data/MIT-LICENSE CHANGED
@@ -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.2) 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
- You should now have a fully functioning single page crud app for Post models.
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
 
@@ -4,31 +4,53 @@ module Backbone
4
4
  module Generators
5
5
  class InstallGenerator < Rails::Generators::Base
6
6
  include Backbone::Generators::ResourceHelpers
7
-
7
+
8
8
  source_root File.expand_path("../templates", __FILE__)
9
-
9
+
10
10
  desc "This generator installs backbone.js with a default folder layout in app/assets/javascripts/backbone"
11
-
11
+
12
12
  class_option :skip_git, :type => :boolean, :aliases => "-G", :default => false,
13
13
  :desc => "Skip Git ignores and keeps"
14
-
14
+
15
15
  def inject_backbone
16
- 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
27
-
49
+
28
50
  def create_app_file
29
51
  template "app.coffee", "app/assets/javascripts/backbone/#{application_name.underscore}.js.coffee"
30
52
  end
31
-
53
+
32
54
  end
33
55
  end
34
- end
56
+ end
@@ -4,5 +4,5 @@ class <%= view_namespace %>.<%= @action.camelize %>View extends Backbone.View
4
4
  template: JST["<%= jst @action %>"]
5
5
 
6
6
  render: ->
7
- $(@el).html(@template())
7
+ @$el.html(@template())
8
8
  return this
@@ -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) ->
@@ -14,4 +14,4 @@
14
14
 
15
15
  </form>
16
16
 
17
- <a href="#/index">Back</a>
17
+ <a href="#/index">Back</a>
@@ -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>
@@ -1,23 +1,23 @@
1
1
  <%= view_namespace %> ||= {}
2
2
 
3
3
  class <%= view_namespace %>.EditView extends Backbone.View
4
- template : JST["<%= jst 'edit' %>"]
4
+ template: JST["<%= jst 'edit' %>"]
5
5
 
6
- events :
7
- "submit #edit-<%= singular_name %>" : "update"
6
+ events:
7
+ "submit #edit-<%= singular_name %>": "update"
8
8
 
9
- update : (e) ->
9
+ update: (e) ->
10
10
  e.preventDefault()
11
11
  e.stopPropagation()
12
12
 
13
13
  @model.save(null,
14
- success : (<%= singular_name %>) =>
14
+ success: (<%= singular_name %>) =>
15
15
  @model = <%= singular_name %>
16
16
  window.location.hash = "/#{@model.id}"
17
17
  )
18
18
 
19
- render : ->
20
- $(@el).html(@template(@model.toJSON() ))
19
+ render: ->
20
+ @$el.html(@template(@model.toJSON() ))
21
21
 
22
22
  this.$("form").backboneLink(@model)
23
23
 
@@ -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
@@ -15,5 +15,5 @@ class <%= view_namespace %>.<%= singular_name.camelize %>View extends Backbone.V
15
15
  return false
16
16
 
17
17
  render: ->
18
- $(@el).html(@template(@model.toJSON() ))
18
+ @$el.html(@template(@model.toJSON() ))
19
19
  return this
@@ -30,7 +30,7 @@ class <%= view_namespace %>.NewView extends Backbone.View
30
30
  )
31
31
 
32
32
  render: ->
33
- $(@el).html(@template(@model.toJSON() ))
33
+ @$el.html(@template(@model.toJSON() ))
34
34
 
35
35
  this.$("form").backboneLink(@model)
36
36
 
@@ -4,5 +4,5 @@ class <%= view_namespace %>.ShowView extends Backbone.View
4
4
  template: JST["<%= jst 'show' %>"]
5
5
 
6
6
  render: ->
7
- $(@el).html(@template(@model.toJSON() ))
7
+ @$el.html(@template(@model.toJSON() ))
8
8
  return this
@@ -1,54 +1,55 @@
1
- // Backbone.js 0.9.2
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, `global`
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 slice/splice.
22
- var slice = Array.prototype.slice;
23
- var splice = Array.prototype.splice;
24
-
25
- // The top-level namespace. All public Backbone classes and modules will
26
- // be attached to this. Exported for both CommonJS and the browser.
27
- var Backbone;
28
- if (typeof exports !== 'undefined') {
29
- Backbone = exports;
30
- } else {
31
- Backbone = root.Backbone = {};
32
- }
43
+ // Create local references to array methods we'll want to use later.
44
+ var array = [];
45
+ var slice = array.slice;
33
46
 
34
47
  // Current version of the library. Keep in sync with `package.json`.
35
- Backbone.VERSION = '0.9.2';
36
-
37
- // Require Underscore, if we're on the server, and it's not already present.
38
- var _ = root._;
39
- if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
40
-
41
- // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
42
- var $ = root.jQuery || root.Zepto || root.ender;
43
-
44
- // Set the JavaScript library that will be used for DOM manipulation and
45
- // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
46
- // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
47
- // alternate JavaScript library (or a mock library for testing your views
48
- // outside of a browser).
49
- Backbone.setDomLibrary = function(lib) {
50
- $ = lib;
51
- };
48
+ Backbone.VERSION = '1.2.0';
49
+
50
+ // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
51
+ // the `$` variable.
52
+ Backbone.$ = $;
52
53
 
53
54
  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
54
55
  // to its previous owner. Returns a reference to this Backbone object.
@@ -58,154 +59,331 @@
58
59
  };
59
60
 
60
61
  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
61
- // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
62
+ // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
62
63
  // set a `X-Http-Method-Override` header.
63
64
  Backbone.emulateHTTP = false;
64
65
 
65
66
  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
66
- // `application/json` requests ... will encode the body as
67
+ // `application/json` requests ... this will encode the body as
67
68
  // `application/x-www-form-urlencoded` instead and will send the model in a
68
69
  // form param named `model`.
69
70
  Backbone.emulateJSON = false;
70
71
 
71
72
  // Backbone.Events
72
- // -----------------
73
-
74
- // Regular expression used to split event strings
75
- var eventSplitter = /\s+/;
73
+ // ---------------
76
74
 
77
75
  // A module that can be mixed in to *any object* in order to provide it with
78
- // custom events. You may bind with `on` or remove with `off` callback functions
79
- // to an event; trigger`-ing an event fires all callbacks in succession.
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.
80
79
  //
81
80
  // var object = {};
82
81
  // _.extend(object, Backbone.Events);
83
82
  // object.on('expand', function(){ alert('expanded'); });
84
83
  // object.trigger('expand');
85
84
  //
86
- var Events = Backbone.Events = {
87
-
88
- // Bind one or more space separated events, `events`, to a `callback`
89
- // function. Passing `"all"` will bind the callback to all events fired.
90
- on: function(events, callback, context) {
91
-
92
- var calls, event, node, tail, list;
93
- if (!callback) return this;
94
- events = events.split(eventSplitter);
95
- calls = this._callbacks || (this._callbacks = {});
96
-
97
- // Create an immutable callback list, allowing traversal during
98
- // modification. The tail is an empty object that will always be used
99
- // as the next node.
100
- while (event = events.shift()) {
101
- list = calls[event];
102
- node = list ? list.tail : {};
103
- node.next = tail = {};
104
- node.context = context;
105
- node.callback = callback;
106
- calls[event] = {tail: tail, next: list ? list.next : node};
85
+ var Events = Backbone.Events = {};
86
+
87
+ // Regular expression used to split event strings.
88
+ var eventSplitter = /\s+/;
89
+
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);
107
101
  }
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);
106
+ }
107
+ } else {
108
+ memo = iteratee(memo, name, callback, opts);
109
+ }
110
+ return memo;
111
+ };
108
112
 
109
- return this;
110
- },
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;
131
+ }
132
+
133
+ return obj;
134
+ };
135
+
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
+ }
150
+
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]];
111
191
 
112
- // Remove one or many callbacks. If `context` is null, removes all callbacks
113
- // with that function. If `callback` is null, removes all callbacks for the
114
- // event. If `events` is null, removes all bound callbacks for all events.
115
- off: function(events, callback, context) {
116
- var event, calls, node, tail, cb, ctx;
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
+ };
117
202
 
118
- // No events, or removing *all* events.
119
- if (!(calls = this._callbacks)) return;
120
- if (!(events || callback || context)) {
121
- delete this._callbacks;
122
- 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];
123
218
  }
219
+ return;
220
+ }
124
221
 
125
- // Loop through the listed events and contexts, splicing them out of the
126
- // linked list of callbacks if appropriate.
127
- events = events ? events.split(eventSplitter) : _.keys(calls);
128
- while (event = events.shift()) {
129
- node = calls[event];
130
- delete calls[event];
131
- if (!node || !(callback || context)) continue;
132
- // Create a new list, omitting the indicated callbacks.
133
- tail = node.tail;
134
- while ((node = node.next) !== tail) {
135
- cb = node.callback;
136
- ctx = node.context;
137
- if ((callback && cb !== callback) || (context && ctx !== context)) {
138
- this.on(event, cb, ctx);
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];
139
245
  }
140
246
  }
141
247
  }
142
248
 
143
- return this;
144
- },
145
-
146
- // Trigger one or many events, firing all bound callbacks. Callbacks are
147
- // passed the same arguments as `trigger` is, apart from the event name
148
- // (unless you're listening on `"all"`, which will cause your callback to
149
- // receive the true name of the event as the first argument).
150
- trigger: function(events) {
151
- var event, node, calls, tail, args, all, rest;
152
- if (!(calls = this._callbacks)) return this;
153
- all = calls.all;
154
- events = events.split(eventSplitter);
155
- rest = slice.call(arguments, 1);
156
-
157
- // For each event, walk through the linked list of callbacks twice,
158
- // first to trigger the event, then to trigger any `"all"` callbacks.
159
- while (event = events.shift()) {
160
- if (node = calls[event]) {
161
- tail = node.tail;
162
- while ((node = node.next) !== tail) {
163
- node.callback.apply(node.context || this, rest);
164
- }
165
- }
166
- if (node = all) {
167
- tail = node.tail;
168
- args = [event].concat(rest);
169
- while ((node = node.next) !== tail) {
170
- node.callback.apply(node.context || this, args);
171
- }
172
- }
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];
173
254
  }
255
+ }
256
+ if (_.size(events)) return events;
257
+ };
174
258
 
175
- return this;
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
+ };
275
+
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();
310
+ if (events) triggerEvents(events, args);
311
+ if (allEvents) triggerEvents(allEvents, [name].concat(args));
312
+ }
313
+ return objEvents;
314
+ };
315
+
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;
176
327
  }
328
+ };
177
329
 
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
+ };
351
+ }
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
+ });
178
357
  };
179
358
 
180
359
  // Aliases for backwards compatibility.
181
360
  Events.bind = Events.on;
182
361
  Events.unbind = Events.off;
183
362
 
363
+ // Allow the `Backbone` object to serve as a global event bus, for folks who
364
+ // want global "pubsub" in a convenient place.
365
+ _.extend(Backbone, Events);
366
+
184
367
  // Backbone.Model
185
368
  // --------------
186
369
 
187
- // 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`)
188
376
  // is automatically generated and assigned for you.
189
377
  var Model = Backbone.Model = function(attributes, options) {
190
- var defaults;
191
- attributes || (attributes = {});
192
- if (options && options.parse) attributes = this.parse(attributes);
193
- if (defaults = getValue(this, 'defaults')) {
194
- attributes = _.extend({}, defaults, attributes);
195
- }
196
- if (options && options.collection) this.collection = options.collection;
378
+ var attrs = attributes || {};
379
+ options || (options = {});
380
+ this.cid = _.uniqueId(this.cidPrefix);
197
381
  this.attributes = {};
198
- this._escapedAttributes = {};
199
- this.cid = _.uniqueId('c');
382
+ if (options.collection) this.collection = options.collection;
383
+ if (options.parse) attrs = this.parse(attrs, options) || {};
384
+ attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
385
+ this.set(attrs, options);
200
386
  this.changed = {};
201
- this._silent = {};
202
- this._pending = {};
203
- this.set(attributes, {silent: true});
204
- // Reset change tracking.
205
- this.changed = {};
206
- this._silent = {};
207
- this._pending = {};
208
- this._previousAttributes = _.clone(this.attributes);
209
387
  this.initialize.apply(this, arguments);
210
388
  };
211
389
 
@@ -215,18 +393,17 @@
215
393
  // A hash of attributes whose current and previous value differ.
216
394
  changed: null,
217
395
 
218
- // A hash of attributes that have silently changed since the last time
219
- // `change` was called. Will become pending attributes on the next call.
220
- _silent: null,
221
-
222
- // A hash of attributes that have changed since the last `'change'` event
223
- // began.
224
- _pending: null,
396
+ // The value returned during the last failed validation.
397
+ validationError: null,
225
398
 
226
399
  // The default name for the JSON `id` attribute is `"id"`. MongoDB and
227
400
  // CouchDB users may want to set this to `"_id"`.
228
401
  idAttribute: 'id',
229
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
+
230
407
  // Initialize is an empty function by default. Override it with your own
231
408
  // initialization logic.
232
409
  initialize: function(){},
@@ -236,6 +413,12 @@
236
413
  return _.clone(this.attributes);
237
414
  },
238
415
 
416
+ // Proxy `Backbone.sync` by default -- but override this if you need
417
+ // custom syncing semantics for *this* particular model.
418
+ sync: function() {
419
+ return Backbone.sync.apply(this, arguments);
420
+ },
421
+
239
422
  // Get the value of an attribute.
240
423
  get: function(attr) {
241
424
  return this.attributes[attr];
@@ -243,10 +426,7 @@
243
426
 
244
427
  // Get the HTML-escaped value of an attribute.
245
428
  escape: function(attr) {
246
- var html;
247
- if (html = this._escapedAttributes[attr]) return html;
248
- var val = this.get(attr);
249
- return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
429
+ return _.escape(this.get(attr));
250
430
  },
251
431
 
252
432
  // Returns `true` if the attribute contains a value that is not null
@@ -255,146 +435,204 @@
255
435
  return this.get(attr) != null;
256
436
  },
257
437
 
258
- // Set a hash of model attributes on the object, firing `"change"` unless
259
- // you choose to silence it.
260
- set: function(key, value, options) {
261
- var attrs, attr, val;
438
+ // Special-cased proxy to underscore's `_.matches` method.
439
+ matches: function(attrs) {
440
+ return !!_.iteratee(attrs, this)(this.attributes);
441
+ },
442
+
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.
446
+ set: function(key, val, options) {
447
+ var attr, attrs, unset, changes, silent, changing, prev, current;
448
+ if (key == null) return this;
262
449
 
263
- // Handle both
264
- if (_.isObject(key) || key == null) {
450
+ // Handle both `"key", value` and `{key: value}` -style arguments.
451
+ if (typeof key === 'object') {
265
452
  attrs = key;
266
- options = value;
453
+ options = val;
267
454
  } else {
268
- attrs = {};
269
- attrs[key] = value;
455
+ (attrs = {})[key] = val;
270
456
  }
271
457
 
272
- // Extract attributes and options.
273
458
  options || (options = {});
274
- if (!attrs) return this;
275
- if (attrs instanceof Model) attrs = attrs.attributes;
276
- if (options.unset) for (attr in attrs) attrs[attr] = void 0;
277
459
 
278
460
  // Run validation.
279
461
  if (!this._validate(attrs, options)) return false;
280
462
 
463
+ // Extract attributes and options.
464
+ unset = options.unset;
465
+ silent = options.silent;
466
+ changes = [];
467
+ changing = this._changing;
468
+ this._changing = true;
469
+
470
+ if (!changing) {
471
+ this._previousAttributes = _.clone(this.attributes);
472
+ this.changed = {};
473
+ }
474
+ current = this.attributes, prev = this._previousAttributes;
475
+
281
476
  // Check for changes of `id`.
282
477
  if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
283
478
 
284
- var changes = options.changes = {};
285
- var now = this.attributes;
286
- var escaped = this._escapedAttributes;
287
- var prev = this._previousAttributes || {};
288
-
289
- // For each `set` attribute...
479
+ // For each `set` attribute, update or delete the current value.
290
480
  for (attr in attrs) {
291
481
  val = attrs[attr];
292
-
293
- // If the new and current value differ, record the change.
294
- if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
295
- delete escaped[attr];
296
- (options.silent ? this._silent : changes)[attr] = true;
297
- }
298
-
299
- // Update or delete the current value.
300
- options.unset ? delete now[attr] : now[attr] = val;
301
-
302
- // If the new and previous value differ, record the change. If not,
303
- // then remove changes for this attribute.
304
- if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
482
+ if (!_.isEqual(current[attr], val)) changes.push(attr);
483
+ if (!_.isEqual(prev[attr], val)) {
305
484
  this.changed[attr] = val;
306
- if (!options.silent) this._pending[attr] = true;
307
485
  } else {
308
486
  delete this.changed[attr];
309
- delete this._pending[attr];
310
487
  }
488
+ unset ? delete current[attr] : current[attr] = val;
311
489
  }
312
490
 
313
- // Fire the `"change"` events.
314
- if (!options.silent) this.change(options);
491
+ // Trigger all relevant attribute changes.
492
+ if (!silent) {
493
+ if (changes.length) this._pending = options;
494
+ for (var i = 0; i < changes.length; i++) {
495
+ this.trigger('change:' + changes[i], this, current[changes[i]], options);
496
+ }
497
+ }
498
+
499
+ // You might be wondering why there's a `while` loop here. Changes can
500
+ // be recursively nested within `"change"` events.
501
+ if (changing) return this;
502
+ if (!silent) {
503
+ while (this._pending) {
504
+ options = this._pending;
505
+ this._pending = false;
506
+ this.trigger('change', this, options);
507
+ }
508
+ }
509
+ this._pending = false;
510
+ this._changing = false;
315
511
  return this;
316
512
  },
317
513
 
318
- // Remove an attribute from the model, firing `"change"` unless you choose
319
- // 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.
320
516
  unset: function(attr, options) {
321
- (options || (options = {})).unset = true;
322
- return this.set(attr, null, options);
517
+ return this.set(attr, void 0, _.extend({}, options, {unset: true}));
323
518
  },
324
519
 
325
- // Clear all attributes on the model, firing `"change"` unless you choose
326
- // to silence it.
520
+ // Clear all attributes on the model, firing `"change"`.
327
521
  clear: function(options) {
328
- (options || (options = {})).unset = true;
329
- return this.set(_.clone(this.attributes), options);
522
+ var attrs = {};
523
+ for (var key in this.attributes) attrs[key] = void 0;
524
+ return this.set(attrs, _.extend({}, options, {unset: true}));
525
+ },
526
+
527
+ // Determine if the model has changed since the last `"change"` event.
528
+ // If you specify an attribute name, determine if that attribute has changed.
529
+ hasChanged: function(attr) {
530
+ if (attr == null) return !_.isEmpty(this.changed);
531
+ return _.has(this.changed, attr);
532
+ },
533
+
534
+ // Return an object containing all the attributes that have changed, or
535
+ // false if there are no changed attributes. Useful for determining what
536
+ // parts of a view need to be updated and/or what attributes need to be
537
+ // persisted to the server. Unset attributes will be set to undefined.
538
+ // You can also pass an attributes object to diff against the model,
539
+ // determining if there *would be* a change.
540
+ changedAttributes: function(diff) {
541
+ if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
542
+ var val, changed = false;
543
+ var old = this._changing ? this._previousAttributes : this.attributes;
544
+ for (var attr in diff) {
545
+ if (_.isEqual(old[attr], (val = diff[attr]))) continue;
546
+ (changed || (changed = {}))[attr] = val;
547
+ }
548
+ return changed;
549
+ },
550
+
551
+ // Get the previous value of an attribute, recorded at the time the last
552
+ // `"change"` event was fired.
553
+ previous: function(attr) {
554
+ if (attr == null || !this._previousAttributes) return null;
555
+ return this._previousAttributes[attr];
556
+ },
557
+
558
+ // Get all of the attributes of the model at the time of the previous
559
+ // `"change"` event.
560
+ previousAttributes: function() {
561
+ return _.clone(this._previousAttributes);
330
562
  },
331
563
 
332
- // Fetch the model from the server. If the server's representation of the
333
- // model differs from its current attributes, they will be overriden,
334
- // 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.
335
566
  fetch: function(options) {
336
567
  options = options ? _.clone(options) : {};
568
+ if (options.parse === void 0) options.parse = true;
337
569
  var model = this;
338
570
  var success = options.success;
339
- options.success = function(resp, status, xhr) {
340
- if (!model.set(model.parse(resp, xhr), options)) return false;
341
- if (success) success(model, resp);
571
+ options.success = function(resp) {
572
+ if (!model.set(model.parse(resp, options), options)) return false;
573
+ if (success) success.call(options.context, model, resp, options);
574
+ model.trigger('sync', model, resp, options);
342
575
  };
343
- options.error = Backbone.wrapError(options.error, model, options);
344
- return (this.sync || Backbone.sync).call(this, 'read', this, options);
576
+ wrapError(this, options);
577
+ return this.sync('read', this, options);
345
578
  },
346
579
 
347
580
  // Set a hash of model attributes, and sync the model to the server.
348
581
  // If the server returns an attributes hash that differs, the model's
349
582
  // state will be `set` again.
350
- save: function(key, value, options) {
351
- var attrs, current;
583
+ save: function(key, val, options) {
584
+ var attrs, method, xhr, attributes = this.attributes, wait;
352
585
 
353
- // Handle both `("key", value)` and `({key: value})` -style calls.
354
- if (_.isObject(key) || key == null) {
586
+ // Handle both `"key", value` and `{key: value}` -style arguments.
587
+ if (key == null || typeof key === 'object') {
355
588
  attrs = key;
356
- options = value;
589
+ options = val;
357
590
  } else {
358
- attrs = {};
359
- attrs[key] = value;
591
+ (attrs = {})[key] = val;
360
592
  }
361
- options = options ? _.clone(options) : {};
362
593
 
363
- // If we're "wait"-ing to set changed attributes, validate early.
364
- if (options.wait) {
594
+ options = _.extend({validate: true}, options);
595
+ wait = options.wait;
596
+
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 {
365
603
  if (!this._validate(attrs, options)) return false;
366
- current = _.clone(this.attributes);
367
604
  }
368
605
 
369
- // Regular saves `set` attributes before persisting to the server.
370
- var silentOptions = _.extend({}, options, {silent: true});
371
- if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
372
- return false;
606
+ // Set temporary attributes if `{wait: true}`.
607
+ if (attrs && wait) {
608
+ this.attributes = _.extend({}, attributes, attrs);
373
609
  }
374
610
 
375
611
  // After a successful server-side save, the client is (optionally)
376
612
  // updated with the server-side state.
613
+ if (options.parse === void 0) options.parse = true;
377
614
  var model = this;
378
615
  var success = options.success;
379
- options.success = function(resp, status, xhr) {
380
- var serverAttrs = model.parse(resp, xhr);
381
- if (options.wait) {
382
- delete options.wait;
383
- serverAttrs = _.extend(attrs || {}, serverAttrs);
384
- }
385
- if (!model.set(serverAttrs, options)) return false;
386
- if (success) {
387
- success(model, resp);
388
- } else {
389
- model.trigger('sync', model, resp, options);
616
+ options.success = function(resp) {
617
+ // Ensure attributes are restored during synchronous saves.
618
+ model.attributes = attributes;
619
+ var serverAttrs = options.parse ? model.parse(resp, options) : resp;
620
+ if (wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
621
+ if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
622
+ return false;
390
623
  }
624
+ if (success) success.call(options.context, model, resp, options);
625
+ model.trigger('sync', model, resp, options);
391
626
  };
627
+ wrapError(this, options);
628
+
629
+ method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
630
+ if (method === 'patch' && !options.attrs) options.attrs = attrs;
631
+ xhr = this.sync(method, this, options);
632
+
633
+ // Restore attributes.
634
+ if (attrs && wait) this.attributes = attributes;
392
635
 
393
- // Finish configuring and sending the Ajax request.
394
- options.error = Backbone.wrapError(options.error, model, options);
395
- var method = this.isNew() ? 'create' : 'update';
396
- var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
397
- if (options.wait) this.set(current, silentOptions);
398
636
  return xhr;
399
637
  },
400
638
 
@@ -405,28 +643,27 @@
405
643
  options = options ? _.clone(options) : {};
406
644
  var model = this;
407
645
  var success = options.success;
646
+ var wait = options.wait;
408
647
 
409
- var triggerDestroy = function() {
648
+ var destroy = function() {
649
+ model.stopListening();
410
650
  model.trigger('destroy', model, model.collection, options);
411
651
  };
412
652
 
413
- if (this.isNew()) {
414
- triggerDestroy();
415
- return false;
416
- }
417
-
418
653
  options.success = function(resp) {
419
- if (options.wait) triggerDestroy();
420
- if (success) {
421
- success(model, resp);
422
- } else {
423
- model.trigger('sync', model, resp, options);
424
- }
654
+ if (wait) destroy();
655
+ if (success) success.call(options.context, model, resp, options);
656
+ if (!model.isNew()) model.trigger('sync', model, resp, options);
425
657
  };
426
658
 
427
- options.error = Backbone.wrapError(options.error, model, options);
428
- var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
429
- if (!options.wait) triggerDestroy();
659
+ var xhr = false;
660
+ if (this.isNew()) {
661
+ _.defer(options.success);
662
+ } else {
663
+ wrapError(this, options);
664
+ xhr = this.sync('delete', this, options);
665
+ }
666
+ if (!wait) destroy();
430
667
  return xhr;
431
668
  },
432
669
 
@@ -434,14 +671,18 @@
434
671
  // using Backbone's restful methods, override this to change the endpoint
435
672
  // that will be called.
436
673
  url: function() {
437
- var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
674
+ var base =
675
+ _.result(this, 'urlRoot') ||
676
+ _.result(this.collection, 'url') ||
677
+ urlError();
438
678
  if (this.isNew()) return base;
439
- 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);
440
681
  },
441
682
 
442
683
  // **parse** converts a response into the hash of attributes to be `set` on
443
684
  // the model. The default implementation is just to pass the response along.
444
- parse: function(resp, xhr) {
685
+ parse: function(resp, options) {
445
686
  return resp;
446
687
  },
447
688
 
@@ -452,119 +693,60 @@
452
693
 
453
694
  // A model is new if it has never been saved to the server, and lacks an id.
454
695
  isNew: function() {
455
- return this.id == null;
456
- },
457
-
458
- // Call this method to manually fire a `"change"` event for this model and
459
- // a `"change:attribute"` event for each changed attribute.
460
- // Calling this will cause all objects observing the model to update.
461
- change: function(options) {
462
- options || (options = {});
463
- var changing = this._changing;
464
- this._changing = true;
465
-
466
- // Silent changes become pending changes.
467
- for (var attr in this._silent) this._pending[attr] = true;
468
-
469
- // Silent changes are triggered.
470
- var changes = _.extend({}, options.changes, this._silent);
471
- this._silent = {};
472
- for (var attr in changes) {
473
- this.trigger('change:' + attr, this, this.get(attr), options);
474
- }
475
- if (changing) return this;
476
-
477
- // Continue firing `"change"` events while there are pending changes.
478
- while (!_.isEmpty(this._pending)) {
479
- this._pending = {};
480
- this.trigger('change', this, options);
481
- // Pending and silent changes still remain.
482
- for (var attr in this.changed) {
483
- if (this._pending[attr] || this._silent[attr]) continue;
484
- delete this.changed[attr];
485
- }
486
- this._previousAttributes = _.clone(this.attributes);
487
- }
488
-
489
- this._changing = false;
490
- return this;
491
- },
492
-
493
- // Determine if the model has changed since the last `"change"` event.
494
- // If you specify an attribute name, determine if that attribute has changed.
495
- hasChanged: function(attr) {
496
- if (!arguments.length) return !_.isEmpty(this.changed);
497
- return _.has(this.changed, attr);
498
- },
499
-
500
- // Return an object containing all the attributes that have changed, or
501
- // false if there are no changed attributes. Useful for determining what
502
- // parts of a view need to be updated and/or what attributes need to be
503
- // persisted to the server. Unset attributes will be set to undefined.
504
- // You can also pass an attributes object to diff against the model,
505
- // determining if there *would be* a change.
506
- changedAttributes: function(diff) {
507
- if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
508
- var val, changed = false, old = this._previousAttributes;
509
- for (var attr in diff) {
510
- if (_.isEqual(old[attr], (val = diff[attr]))) continue;
511
- (changed || (changed = {}))[attr] = val;
512
- }
513
- return changed;
514
- },
515
-
516
- // Get the previous value of an attribute, recorded at the time the last
517
- // `"change"` event was fired.
518
- previous: function(attr) {
519
- if (!arguments.length || !this._previousAttributes) return null;
520
- return this._previousAttributes[attr];
521
- },
522
-
523
- // Get all of the attributes of the model at the time of the previous
524
- // `"change"` event.
525
- previousAttributes: function() {
526
- return _.clone(this._previousAttributes);
696
+ return !this.has(this.idAttribute);
527
697
  },
528
698
 
529
- // Check if the model is currently in a valid state. It's only possible to
530
- // get into an *invalid* state if you're using silent changes.
531
- isValid: function() {
532
- return !this.validate(this.attributes);
699
+ // Check if the model is currently in a valid state.
700
+ isValid: function(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. If a specific `error` callback has
537
- // been passed, call that instead of firing the general `"error"` event.
705
+ // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
538
706
  _validate: function(attrs, options) {
539
- if (options.silent || !this.validate) return true;
707
+ if (!options.validate || !this.validate) return true;
540
708
  attrs = _.extend({}, this.attributes, attrs);
541
- var error = this.validate(attrs, options);
709
+ var error = this.validationError = this.validate(attrs, options) || null;
542
710
  if (!error) return true;
543
- if (options && options.error) {
544
- options.error(this, error, options);
545
- } else {
546
- this.trigger('error', this, error, options);
547
- }
711
+ this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
548
712
  return false;
549
713
  }
550
714
 
551
715
  });
552
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
+
553
724
  // Backbone.Collection
554
725
  // -------------------
555
726
 
556
- // Provides a standard collection class for our sets of models, ordered
557
- // 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
558
736
  // its models in sort order, as they're added and removed.
559
737
  var Collection = Backbone.Collection = function(models, options) {
560
738
  options || (options = {});
561
739
  if (options.model) this.model = options.model;
562
- if (options.comparator) this.comparator = options.comparator;
740
+ if (options.comparator !== void 0) this.comparator = options.comparator;
563
741
  this._reset();
564
742
  this.initialize.apply(this, arguments);
565
- if (models) this.reset(models, {silent: true, parse: options.parse});
743
+ if (models) this.reset(models, _.extend({silent: true}, options));
566
744
  };
567
745
 
746
+ // Default options for `Collection#set`.
747
+ var setOptions = {add: true, remove: true, merge: true};
748
+ var addOptions = {add: true, remove: false};
749
+
568
750
  // Define the Collection's inheritable methods.
569
751
  _.extend(Collection.prototype, Events, {
570
752
 
@@ -582,85 +764,147 @@
582
764
  return this.map(function(model){ return model.toJSON(options); });
583
765
  },
584
766
 
585
- // Add a model, or list of models to the set. Pass **silent** to avoid
586
- // firing the `add` event for every new model.
767
+ // Proxy `Backbone.sync` by default.
768
+ sync: function() {
769
+ return Backbone.sync.apply(this, arguments);
770
+ },
771
+
772
+ // Add a model, or list of models to the set.
587
773
  add: function(models, options) {
588
- var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
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);
589
781
  options || (options = {});
590
- models = _.isArray(models) ? models.slice() : [models];
782
+ removed = this._removeModels(models, options);
783
+ if (!options.silent && removed) this.trigger('update', this, options);
784
+ return singular ? models[0] : models;
785
+ },
591
786
 
592
- // Begin by turning bare objects into model references, and preventing
593
- // invalid models or duplicate models from being added.
594
- for (i = 0, length = models.length; i < length; i++) {
595
- if (!(model = models[i] = this._prepareModel(models[i], options))) {
596
- throw new Error("Can't add an invalid model to a collection");
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;
806
+
807
+ // Turn bare objects into model references, and prevent invalid models
808
+ // from being added.
809
+ for (var i = 0; i < models.length; i++) {
810
+ attrs = models[i];
811
+
812
+ // If a duplicate is found, prevent it from being added and
813
+ // optionally merge it into the existing model.
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;
821
+ }
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);
597
830
  }
598
- cid = model.cid;
599
- id = model.id;
600
- if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
601
- dups.push(i);
602
- continue;
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;
603
841
  }
604
- cids[cid] = ids[id] = model;
842
+
843
+ modelMap[id] = true;
605
844
  }
606
845
 
607
- // Remove duplicates.
608
- i = dups.length;
609
- while (i--) {
610
- models.splice(dups[i], 1);
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);
611
852
  }
612
853
 
613
- // Listen to added models' events, and index models for lookup by
614
- // `id` and by `cid`.
615
- for (i = 0, length = models.length; i < length; i++) {
616
- (model = models[i]).on('all', this._onModelEvent, this);
617
- this._byCid[model.cid] = model;
618
- if (model.id != null) this._byId[model.id] = model;
854
+ // See if sorting is needed, update `length` and splice in new models.
855
+ if (toAdd.length || orderChanged) {
856
+ if (sortable) sort = true;
857
+ this.length += toAdd.length;
858
+ if (at != null) {
859
+ for (var i = 0; i < toAdd.length; i++) {
860
+ this.models.splice(at + i, 0, toAdd[i]);
861
+ }
862
+ } else {
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
+ }
868
+ }
619
869
  }
620
870
 
621
- // Insert models into the collection, re-sorting if needed, and triggering
622
- // `add` events unless silenced.
623
- this.length += length;
624
- index = options.at != null ? options.at : this.models.length;
625
- splice.apply(this.models, [index, 0].concat(models));
626
- if (this.comparator) this.sort({silent: true});
627
- if (options.silent) return this;
628
- for (i = 0, length = this.models.length; i < length; i++) {
629
- if (!cids[(model = this.models[i]).cid]) continue;
630
- options.index = i;
631
- model.trigger('add', model, this, options);
871
+ // Silently sort the collection if appropriate.
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);
632
883
  }
633
- return this;
884
+
885
+ // Return the added (or merged) model (or models).
886
+ return singular ? models[0] : models;
634
887
  },
635
888
 
636
- // Remove a model, or a list of models from the set. Pass silent to avoid
637
- // firing the `remove` event for every model removed.
638
- remove: function(models, options) {
639
- var i, l, index, model;
640
- options || (options = {});
641
- models = _.isArray(models) ? models.slice() : [models];
642
- for (i = 0, l = models.length; i < l; i++) {
643
- model = this.getByCid(models[i]) || this.get(models[i]);
644
- if (!model) continue;
645
- delete this._byId[model.id];
646
- delete this._byCid[model.cid];
647
- index = this.indexOf(model);
648
- this.models.splice(index, 1);
649
- this.length--;
650
- if (!options.silent) {
651
- options.index = index;
652
- model.trigger('remove', model, this, options);
653
- }
654
- 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);
655
897
  }
656
- 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;
657
903
  },
658
904
 
659
905
  // Add a model to the end of the collection.
660
906
  push: function(model, options) {
661
- model = this._prepareModel(model, options);
662
- this.add(model, options);
663
- return model;
907
+ return this.add(model, _.extend({at: this.length}, options));
664
908
  },
665
909
 
666
910
  // Remove a model from the end of the collection.
@@ -672,9 +916,7 @@
672
916
 
673
917
  // Add a model to the beginning of the collection.
674
918
  unshift: function(model, options) {
675
- model = this._prepareModel(model, options);
676
- this.add(model, _.extend({at: 0}, options));
677
- return model;
919
+ return this.add(model, _.extend({at: 0}, options));
678
920
  },
679
921
 
680
922
  // Remove a model from the beginning of the collection.
@@ -684,102 +926,93 @@
684
926
  return model;
685
927
  },
686
928
 
687
- // Get a model from the set by id.
688
- get: function(id) {
689
- if (id == null) return void 0;
690
- return this._byId[id.id != null ? id.id : id];
929
+ // Slice out a sub-array of models from the collection.
930
+ slice: function() {
931
+ return slice.apply(this.models, arguments);
691
932
  },
692
933
 
693
- // Get a model from the set by client id.
694
- getByCid: function(cid) {
695
- return cid && this._byCid[cid.cid || cid];
934
+ // Get a model from the set by id.
935
+ get: function(obj) {
936
+ if (obj == null) return void 0;
937
+ var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
938
+ return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
696
939
  },
697
940
 
698
941
  // Get the model at the given index.
699
942
  at: function(index) {
943
+ if (index < 0) index += this.length;
700
944
  return this.models[index];
701
945
  },
702
946
 
703
- // Return models with matching attributes. Useful for simple cases of `filter`.
704
- where: function(attrs) {
705
- if (_.isEmpty(attrs)) return [];
706
- return this.filter(function(model) {
707
- for (var key in attrs) {
708
- if (attrs[key] !== model.get(key)) return false;
709
- }
710
- 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);
711
953
  });
712
954
  },
713
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
+
714
962
  // Force the collection to re-sort itself. You don't need to call this under
715
963
  // normal circumstances, as the set will maintain sort order as each item
716
964
  // is added.
717
965
  sort: function(options) {
718
- options || (options = {});
719
966
  if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
720
- var boundComparator = _.bind(this.comparator, this);
721
- if (this.comparator.length == 1) {
722
- this.models = this.sortBy(boundComparator);
967
+ options || (options = {});
968
+
969
+ // Run sort based on type of `comparator`.
970
+ if (_.isString(this.comparator) || this.comparator.length === 1) {
971
+ this.models = this.sortBy(this.comparator, this);
723
972
  } else {
724
- this.models.sort(boundComparator);
973
+ this.models.sort(_.bind(this.comparator, this));
725
974
  }
726
- if (!options.silent) this.trigger('reset', this, options);
975
+
976
+ if (!options.silent) this.trigger('sort', this, options);
727
977
  return this;
728
978
  },
729
979
 
730
980
  // Pluck an attribute from each model in the collection.
731
981
  pluck: function(attr) {
732
- return _.map(this.models, function(model){ return model.get(attr); });
733
- },
734
-
735
- // When you have more items than you want to add or remove individually,
736
- // you can reset the entire set with a new list of models, without firing
737
- // any `add` or `remove` events. Fires `reset` when finished.
738
- reset: function(models, options) {
739
- models || (models = []);
740
- options || (options = {});
741
- for (var i = 0, l = this.models.length; i < l; i++) {
742
- this._removeReference(this.models[i]);
743
- }
744
- this._reset();
745
- this.add(models, _.extend({silent: true}, options));
746
- if (!options.silent) this.trigger('reset', this, options);
747
- return this;
982
+ return _.invoke(this.models, 'get', attr);
748
983
  },
749
984
 
750
985
  // Fetch the default set of models for this collection, resetting the
751
- // collection when they arrive. If `add: true` is passed, appends the
752
- // models to the collection instead of resetting.
986
+ // collection when they arrive. If `reset: true` is passed, the response
987
+ // data will be passed through the `reset` method instead of `set`.
753
988
  fetch: function(options) {
754
989
  options = options ? _.clone(options) : {};
755
- if (options.parse === undefined) options.parse = true;
756
- var collection = this;
990
+ if (options.parse === void 0) options.parse = true;
757
991
  var success = options.success;
758
- options.success = function(resp, status, xhr) {
759
- collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
760
- if (success) success(collection, resp);
992
+ var collection = this;
993
+ options.success = function(resp) {
994
+ var method = options.reset ? 'reset' : 'set';
995
+ collection[method](resp, options);
996
+ if (success) success.call(options.context, collection, resp, options);
997
+ collection.trigger('sync', collection, resp, options);
761
998
  };
762
- options.error = Backbone.wrapError(options.error, collection, options);
763
- return (this.sync || Backbone.sync).call(this, 'read', this, options);
999
+ wrapError(this, options);
1000
+ return this.sync('read', this, options);
764
1001
  },
765
1002
 
766
1003
  // Create a new instance of a model in this collection. Add the model to the
767
1004
  // collection immediately, unless `wait: true` is passed, in which case we
768
1005
  // wait for the server to agree.
769
1006
  create: function(model, options) {
770
- var coll = this;
771
1007
  options = options ? _.clone(options) : {};
772
- model = this._prepareModel(model, options);
773
- if (!model) return false;
774
- if (!options.wait) coll.add(model, options);
1008
+ var wait = options.wait;
1009
+ if (!(model = this._prepareModel(model, options))) return false;
1010
+ if (!wait) this.add(model, options);
1011
+ var collection = this;
775
1012
  var success = options.success;
776
- options.success = function(nextModel, resp, xhr) {
777
- if (options.wait) coll.add(nextModel, options);
778
- if (success) {
779
- success(nextModel, resp);
780
- } else {
781
- nextModel.trigger('sync', model, resp, options);
782
- }
1013
+ options.success = function(model, resp, callbackOpts) {
1014
+ if (wait) collection.add(model, callbackOpts);
1015
+ if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
783
1016
  };
784
1017
  model.save(null, options);
785
1018
  return model;
@@ -787,44 +1020,90 @@
787
1020
 
788
1021
  // **parse** converts a response into a list of models to be added to the
789
1022
  // collection. The default implementation is just to pass it through.
790
- parse: function(resp, xhr) {
1023
+ parse: function(resp, options) {
791
1024
  return resp;
792
1025
  },
793
1026
 
794
- // Proxy to _'s chain. Can't be proxied the same way the rest of the
795
- // underscore methods are proxied because it relies on the underscore
796
- // constructor.
797
- chain: function () {
798
- return _(this.models).chain();
1027
+ // Create a new collection with an identical list of models as this one.
1028
+ clone: function() {
1029
+ return new this.constructor(this.models, {
1030
+ model: this.model,
1031
+ comparator: this.comparator
1032
+ });
1033
+ },
1034
+
1035
+ // Define how to uniquely identify models in the collection.
1036
+ modelId: function (attrs) {
1037
+ return attrs[this.model.prototype.idAttribute || 'id'];
799
1038
  },
800
1039
 
801
- // Reset all internal state. Called when the collection is reset.
802
- _reset: function(options) {
1040
+ // Private method to reset all internal state. Called when the collection
1041
+ // is first initialized or reset.
1042
+ _reset: function() {
803
1043
  this.length = 0;
804
1044
  this.models = [];
805
1045
  this._byId = {};
806
- this._byCid = {};
807
1046
  },
808
1047
 
809
- // Prepare a model or hash of attributes to be added to this collection.
810
- _prepareModel: function(model, options) {
811
- options || (options = {});
812
- if (!(model instanceof Model)) {
813
- var attrs = model;
814
- options.collection = this;
815
- model = new this.model(attrs, options);
816
- if (!model._validate(model.attributes, options)) model = false;
817
- } else if (!model.collection) {
818
- model.collection = this;
1048
+ // Prepare a hash of attributes (or other model) to be added to this
1049
+ // collection.
1050
+ _prepareModel: function(attrs, options) {
1051
+ if (this._isModel(attrs)) {
1052
+ if (!attrs.collection) attrs.collection = this;
1053
+ return attrs;
819
1054
  }
820
- return model;
1055
+ options = options ? _.clone(options) : {};
1056
+ options.collection = this;
1057
+ var model = new this.model(attrs, options);
1058
+ if (!model.validationError) return model;
1059
+ this.trigger('invalid', this, model.validationError, options);
1060
+ return false;
821
1061
  },
822
1062
 
823
- // Internal method to remove a model's ties to a collection.
824
- _removeReference: function(model) {
825
- if (this == model.collection) {
826
- delete model.collection;
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;
827
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);
1102
+ },
1103
+
1104
+ // Internal method to sever a model's ties to a collection.
1105
+ _removeReference: function(model, options) {
1106
+ if (this === model.collection) delete model.collection;
828
1107
  model.off('all', this._onModelEvent, this);
829
1108
  },
830
1109
 
@@ -833,13 +1112,15 @@
833
1112
  // events simply proxy through. "add" and "remove" events that originate
834
1113
  // in other collections are ignored.
835
1114
  _onModelEvent: function(event, model, collection, options) {
836
- if ((event == 'add' || event == 'remove') && collection != this) return;
837
- if (event == 'destroy') {
838
- this.remove(model, options);
839
- }
840
- if (model && event === 'change:' + model.idAttribute) {
841
- delete this._byId[model.previous(model.idAttribute)];
842
- this._byId[model.id] = model;
1115
+ if ((event === 'add' || event === 'remove') && collection !== this) return;
1116
+ if (event === 'destroy') this.remove(model, options);
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
+ }
843
1124
  }
844
1125
  this.trigger.apply(this, arguments);
845
1126
  }
@@ -847,21 +1128,289 @@
847
1128
  });
848
1129
 
849
1130
  // Underscore methods that we want to implement on the Collection.
850
- var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
851
- 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
852
- 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
853
- 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
854
- 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
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 };
855
1140
 
856
1141
  // Mix in each Underscore method as a proxy to `Collection#models`.
857
- _.each(methods, function(method) {
858
- Collection.prototype[method] = function() {
859
- return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
1142
+ addUnderscoreMethods(Collection, collectionMethods, 'models');
1143
+
1144
+ // Underscore methods that take a property name as an argument.
1145
+ var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
1146
+
1147
+ // Use attributes instead of properties.
1148
+ _.each(attributeMethods, function(method) {
1149
+ if (!_[method]) return;
1150
+ Collection.prototype[method] = function(value, context) {
1151
+ var iterator = _.isFunction(value) ? value : function(model) {
1152
+ return model.get(value);
1153
+ };
1154
+ return _[method](this.models, iterator, context);
860
1155
  };
861
1156
  });
862
1157
 
1158
+ // Backbone.View
1159
+ // -------------
1160
+
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');
1173
+ options || (options = {});
1174
+ _.extend(this, _.pick(options, viewOptions));
1175
+ this._ensureElement();
1176
+ this.initialize.apply(this, arguments);
1177
+ };
1178
+
1179
+ // Cached regex to split keys for `delegate`.
1180
+ var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1181
+
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
+
863
1412
  // Backbone.Router
864
- // -------------------
1413
+ // ---------------
865
1414
 
866
1415
  // Routers map faux-URLs to actions, and fire events when routes are
867
1416
  // matched. Creating a new one sets its `routes` hash, if not set statically.
@@ -874,9 +1423,10 @@
874
1423
 
875
1424
  // Cached regular expressions for matching named param parts and splatted
876
1425
  // parts of route strings.
877
- var namedParam = /:\w+/g;
1426
+ var optionalParam = /\((.*?)\)/g;
1427
+ var namedParam = /(\(\?)?:\w+/g;
878
1428
  var splatParam = /\*\w+/g;
879
- var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
1429
+ var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
880
1430
 
881
1431
  // Set up all inheritable **Backbone.Router** properties and methods.
882
1432
  _.extend(Router.prototype, Events, {
@@ -892,21 +1442,34 @@
892
1442
  // });
893
1443
  //
894
1444
  route: function(route, name, callback) {
895
- Backbone.history || (Backbone.history = new History);
896
1445
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
1446
+ if (_.isFunction(name)) {
1447
+ callback = name;
1448
+ name = '';
1449
+ }
897
1450
  if (!callback) callback = this[name];
898
- Backbone.history.route(route, _.bind(function(fragment) {
899
- var args = this._extractParameters(route, fragment);
900
- callback && callback.apply(this, args);
901
- this.trigger.apply(this, ['route:' + name].concat(args));
902
- Backbone.history.trigger('route', this, name, args);
903
- }, 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
+ });
904
1460
  return this;
905
1461
  },
906
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
+
907
1469
  // Simple proxy to `Backbone.history` to save a fragment into the history.
908
1470
  navigate: function(fragment, options) {
909
1471
  Backbone.history.navigate(fragment, options);
1472
+ return this;
910
1473
  },
911
1474
 
912
1475
  // Bind all defined routes to `Backbone.history`. We have to reverse the
@@ -914,12 +1477,10 @@
914
1477
  // routes can be defined at the bottom of the route map.
915
1478
  _bindRoutes: function() {
916
1479
  if (!this.routes) return;
917
- var routes = [];
918
- for (var route in this.routes) {
919
- routes.unshift([route, this.routes[route]]);
920
- }
921
- for (var i = 0, l = routes.length; i < l; i++) {
922
- this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
1480
+ this.routes = _.result(this, 'routes');
1481
+ var route, routes = _.keys(this.routes);
1482
+ while ((route = routes.pop()) != null) {
1483
+ this.route(route, this.routes[route]);
923
1484
  }
924
1485
  },
925
1486
 
@@ -927,15 +1488,24 @@
927
1488
  // against the current location hash.
928
1489
  _routeToRegExp: function(route) {
929
1490
  route = route.replace(escapeRegExp, '\\$&')
930
- .replace(namedParam, '([^\/]+)')
931
- .replace(splatParam, '(.*?)');
932
- return new RegExp('^' + route + '$');
1491
+ .replace(optionalParam, '(?:$1)?')
1492
+ .replace(namedParam, function(match, optional) {
1493
+ return optional ? match : '([^/?]+)';
1494
+ })
1495
+ .replace(splatParam, '([^?]*?)');
1496
+ return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
933
1497
  },
934
1498
 
935
1499
  // Given a route, and a URL fragment that it matches, return the array of
936
- // extracted parameters.
1500
+ // extracted decoded parameters. Empty or unmatched parameters will be
1501
+ // treated as `null` to normalize cross-browser behavior.
937
1502
  _extractParameters: function(route, fragment) {
938
- 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
+ });
939
1509
  }
940
1510
 
941
1511
  });
@@ -943,18 +1513,30 @@
943
1513
  // Backbone.History
944
1514
  // ----------------
945
1515
 
946
- // Handles cross-browser history management, based on URL fragments. If the
947
- // 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.
948
1521
  var History = Backbone.History = function() {
949
1522
  this.handlers = [];
950
1523
  _.bindAll(this, 'checkUrl');
1524
+
1525
+ // Ensure that `History` can be used outside of the browser.
1526
+ if (typeof window !== 'undefined') {
1527
+ this.location = window.location;
1528
+ this.history = window.history;
1529
+ }
951
1530
  };
952
1531
 
953
- // Cached regex for cleaning leading hashes and slashes .
954
- var routeStripper = /^[#\/]/;
1532
+ // Cached regex for stripping a leading hash/slash and trailing space.
1533
+ var routeStripper = /^[#\/]|\s+$/g;
1534
+
1535
+ // Cached regex for stripping leading and trailing slashes.
1536
+ var rootStripper = /^\/+|\/+$/g;
955
1537
 
956
- // Cached regex for detecting MSIE.
957
- var isExplorer = /msie [\w.]+/;
1538
+ // Cached regex for stripping urls of hash.
1539
+ var pathStripper = /#.*$/;
958
1540
 
959
1541
  // Has the history handling already been started?
960
1542
  History.started = false;
@@ -966,92 +1548,157 @@
966
1548
  // twenty times a second.
967
1549
  interval: 50,
968
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
+
969
1578
  // Gets the true hash value. Cannot use location.hash directly due to bug
970
1579
  // in Firefox where location.hash will always be decoded.
971
- getHash: function(windowOverride) {
972
- var loc = windowOverride ? windowOverride.location : window.location;
973
- var match = loc.href.match(/#(.*)$/);
1580
+ getHash: function(window) {
1581
+ var match = (window || this).location.href.match(/#(.*)$/);
974
1582
  return match ? match[1] : '';
975
1583
  },
976
1584
 
977
- // Get the cross-browser normalized URL fragment, either from the URL,
978
- // the hash, or the override.
979
- 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) {
980
1595
  if (fragment == null) {
981
- if (this._hasPushState || forcePushState) {
982
- fragment = window.location.pathname;
983
- var search = window.location.search;
984
- if (search) fragment += search;
1596
+ if (this._usePushState || !this._wantsHashChange) {
1597
+ fragment = this.getPath();
985
1598
  } else {
986
1599
  fragment = this.getHash();
987
1600
  }
988
1601
  }
989
- if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
990
1602
  return fragment.replace(routeStripper, '');
991
1603
  },
992
1604
 
993
1605
  // Start the hash change handling, returning `true` if the current URL matches
994
1606
  // an existing route, and `false` otherwise.
995
1607
  start: function(options) {
996
- 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');
997
1609
  History.started = true;
998
1610
 
999
1611
  // Figure out the initial configuration. Do we need an iframe?
1000
1612
  // Is pushState desired ... is it available?
1001
- this.options = _.extend({}, {root: '/'}, this.options, options);
1613
+ this.options = _.extend({root: '/'}, this.options, options);
1614
+ this.root = this.options.root;
1002
1615
  this._wantsHashChange = this.options.hashChange !== false;
1616
+ this._hasHashChange = 'onhashchange' in window;
1617
+ this._useHashChange = this._wantsHashChange && this._hasHashChange;
1003
1618
  this._wantsPushState = !!this.options.pushState;
1004
- this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
1005
- var fragment = this.getFragment();
1006
- var docMode = document.documentMode;
1007
- var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1008
-
1009
- if (oldIE) {
1010
- this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
1011
- this.navigate(fragment);
1619
+ this._hasPushState = !!(this.history && this.history.pushState);
1620
+ this._usePushState = this._wantsPushState && this._hasPushState;
1621
+ this.fragment = this.getFragment();
1622
+
1623
+ // Normalize root to always include a leading and trailing slash.
1624
+ this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1625
+
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
+
1012
1644
  }
1013
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;
1659
+ }
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
+
1014
1666
  // Depending on whether we're using pushState or hashes, and whether
1015
1667
  // 'onhashchange' is supported, determine how we check the URL state.
1016
- if (this._hasPushState) {
1017
- $(window).bind('popstate', this.checkUrl);
1018
- } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1019
- $(window).bind('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);
1020
1672
  } else if (this._wantsHashChange) {
1021
1673
  this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1022
1674
  }
1023
1675
 
1024
- // Determine if we need to change the base url, for a pushState link
1025
- // opened by a non-pushState browser.
1026
- this.fragment = fragment;
1027
- var loc = window.location;
1028
- var atRoot = loc.pathname == this.options.root;
1029
-
1030
- // If we've started off with a route from a `pushState`-enabled browser,
1031
- // but we're currently in a browser that doesn't support it...
1032
- if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1033
- this.fragment = this.getFragment(null, true);
1034
- window.location.replace(this.options.root + '#' + this.fragment);
1035
- // Return immediately as browser will do redirect to new url
1036
- return true;
1037
-
1038
- // Or if we've started out with a hash-based route, but we're currently
1039
- // in a browser where it could be `pushState`-based instead...
1040
- } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1041
- this.fragment = this.getHash().replace(routeStripper, '');
1042
- window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
1043
- }
1044
-
1045
- if (!this.options.silent) {
1046
- return this.loadUrl();
1047
- }
1676
+ if (!this.options.silent) return this.loadUrl();
1048
1677
  },
1049
1678
 
1050
1679
  // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1051
1680
  // but possibly useful for unit testing Routers.
1052
1681
  stop: function() {
1053
- $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
1054
- 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);
1055
1702
  History.started = false;
1056
1703
  },
1057
1704
 
@@ -1065,24 +1712,31 @@
1065
1712
  // calls `loadUrl`, normalizing across the hidden iframe.
1066
1713
  checkUrl: function(e) {
1067
1714
  var current = this.getFragment();
1068
- if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
1069
- if (current == this.fragment) return false;
1715
+
1716
+ // If the user pressed the back button, the iframe's hash will have
1717
+ // changed and we should use that for comparison.
1718
+ if (current === this.fragment && this.iframe) {
1719
+ current = this.getHash(this.iframe);
1720
+ }
1721
+
1722
+ if (current === this.fragment) return false;
1070
1723
  if (this.iframe) this.navigate(current);
1071
- this.loadUrl() || this.loadUrl(this.getHash());
1724
+ this.loadUrl();
1072
1725
  },
1073
1726
 
1074
1727
  // Attempt to load the current URL fragment. If a route succeeds with a
1075
1728
  // match, returns `true`. If no defined routes matches the fragment,
1076
1729
  // returns `false`.
1077
- loadUrl: function(fragmentOverride) {
1078
- var fragment = this.fragment = this.getFragment(fragmentOverride);
1079
- 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) {
1080
1735
  if (handler.route.test(fragment)) {
1081
1736
  handler.callback(fragment);
1082
1737
  return true;
1083
1738
  }
1084
1739
  });
1085
- return matched;
1086
1740
  },
1087
1741
 
1088
1742
  // Save a fragment into the hash history, or replace the URL state if the
@@ -1094,338 +1748,121 @@
1094
1748
  // you wish to modify the current URL without adding an entry to the history.
1095
1749
  navigate: function(fragment, options) {
1096
1750
  if (!History.started) return false;
1097
- if (!options || options === true) options = {trigger: options};
1098
- var frag = (fragment || '').replace(routeStripper, '');
1099
- if (this.fragment == frag) return;
1751
+ if (!options || options === true) options = {trigger: !!options};
1752
+
1753
+ // Normalize the fragment.
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
+
1766
+ if (this.fragment === fragment) return;
1767
+ this.fragment = fragment;
1100
1768
 
1101
1769
  // If pushState is available, we use it to set the fragment as a real URL.
1102
- if (this._hasPushState) {
1103
- if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
1104
- this.fragment = frag;
1105
- window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
1770
+ if (this._usePushState) {
1771
+ this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1106
1772
 
1107
1773
  // If hash changes haven't been explicitly disabled, update the hash
1108
1774
  // fragment to store history.
1109
1775
  } else if (this._wantsHashChange) {
1110
- this.fragment = frag;
1111
- this._updateHash(window.location, frag, options.replace);
1112
- if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
1113
- // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
1114
- // When replace is true, we don't want this.
1115
- if(!options.replace) this.iframe.document.open().close();
1116
- this._updateHash(this.iframe.location, frag, options.replace);
1776
+ this._updateHash(this.location, fragment, options.replace);
1777
+ if (this.iframe && (fragment !== this.getHash(this.iframe))) {
1778
+ // Opening and closing the iframe tricks IE7 and earlier to push a
1779
+ // history entry on hash-tag change. When replace is true, we don't
1780
+ // want this.
1781
+ if (!options.replace) this.iframe.document.open().close();
1782
+ this._updateHash(this.iframe.location, fragment, options.replace);
1117
1783
  }
1118
1784
 
1119
1785
  // If you've told us that you explicitly don't want fallback hashchange-
1120
1786
  // based history, then `navigate` becomes a page refresh.
1121
1787
  } else {
1122
- window.location.assign(this.options.root + fragment);
1788
+ return this.location.assign(url);
1123
1789
  }
1124
- if (options.trigger) this.loadUrl(fragment);
1790
+ if (options.trigger) return this.loadUrl(fragment);
1125
1791
  },
1126
1792
 
1127
1793
  // Update the hash location, either replacing the current entry, or adding
1128
1794
  // a new one to the browser history.
1129
1795
  _updateHash: function(location, fragment, replace) {
1130
1796
  if (replace) {
1131
- location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
1797
+ var href = location.href.replace(/(javascript:|#).*$/, '');
1798
+ location.replace(href + '#' + fragment);
1132
1799
  } else {
1133
- location.hash = fragment;
1134
- }
1135
- }
1136
- });
1137
-
1138
- // Backbone.View
1139
- // -------------
1140
-
1141
- // Creating a Backbone.View creates its initial element outside of the DOM,
1142
- // if an existing element is not provided...
1143
- var View = Backbone.View = function(options) {
1144
- this.cid = _.uniqueId('view');
1145
- this._configure(options || {});
1146
- this._ensureElement();
1147
- this.initialize.apply(this, arguments);
1148
- this.delegateEvents();
1149
- };
1150
-
1151
- // Cached regex to split keys for `delegate`.
1152
- var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1153
-
1154
- // List of view options to be merged as properties.
1155
- var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
1156
-
1157
- // Set up all inheritable **Backbone.View** properties and methods.
1158
- _.extend(View.prototype, Events, {
1159
-
1160
- // The default `tagName` of a View's element is `"div"`.
1161
- tagName: 'div',
1162
-
1163
- // jQuery delegate for element lookup, scoped to DOM elements within the
1164
- // current view. This should be prefered to global lookups where possible.
1165
- $: function(selector) {
1166
- return this.$el.find(selector);
1167
- },
1168
-
1169
- // Initialize is an empty function by default. Override it with your own
1170
- // initialization logic.
1171
- initialize: function(){},
1172
-
1173
- // **render** is the core function that your view should override, in order
1174
- // to populate its element (`this.el`), with the appropriate HTML. The
1175
- // convention is for **render** to always return `this`.
1176
- render: function() {
1177
- return this;
1178
- },
1179
-
1180
- // Remove this view from the DOM. Note that the view isn't present in the
1181
- // DOM by default, so calling this method may be a no-op.
1182
- remove: function() {
1183
- this.$el.remove();
1184
- return this;
1185
- },
1186
-
1187
- // For small amounts of DOM Elements, where a full-blown template isn't
1188
- // needed, use **make** to manufacture elements, one at a time.
1189
- //
1190
- // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
1191
- //
1192
- make: function(tagName, attributes, content) {
1193
- var el = document.createElement(tagName);
1194
- if (attributes) $(el).attr(attributes);
1195
- if (content) $(el).html(content);
1196
- return el;
1197
- },
1198
-
1199
- // Change the view's element (`this.el` property), including event
1200
- // re-delegation.
1201
- setElement: function(element, delegate) {
1202
- if (this.$el) this.undelegateEvents();
1203
- this.$el = (element instanceof $) ? element : $(element);
1204
- this.el = this.$el[0];
1205
- if (delegate !== false) this.delegateEvents();
1206
- return this;
1207
- },
1208
-
1209
- // Set callbacks, where `this.events` is a hash of
1210
- //
1211
- // *{"event selector": "callback"}*
1212
- //
1213
- // {
1214
- // 'mousedown .title': 'edit',
1215
- // 'click .button': 'save'
1216
- // 'click .open': function(e) { ... }
1217
- // }
1218
- //
1219
- // pairs. Callbacks will be bound to the view, with `this` set properly.
1220
- // Uses event delegation for efficiency.
1221
- // Omitting the selector binds the event to `this.el`.
1222
- // This only works for delegate-able events: not `focus`, `blur`, and
1223
- // not `change`, `submit`, and `reset` in Internet Explorer.
1224
- delegateEvents: function(events) {
1225
- if (!(events || (events = getValue(this, 'events')))) return;
1226
- this.undelegateEvents();
1227
- for (var key in events) {
1228
- var method = events[key];
1229
- if (!_.isFunction(method)) method = this[events[key]];
1230
- if (!method) throw new Error('Method "' + events[key] + '" does not exist');
1231
- var match = key.match(delegateEventSplitter);
1232
- var eventName = match[1], selector = match[2];
1233
- method = _.bind(method, this);
1234
- eventName += '.delegateEvents' + this.cid;
1235
- if (selector === '') {
1236
- this.$el.bind(eventName, method);
1237
- } else {
1238
- this.$el.delegate(selector, eventName, method);
1239
- }
1240
- }
1241
- },
1242
-
1243
- // Clears all callbacks previously bound to the view with `delegateEvents`.
1244
- // You usually don't need to use this, but may wish to if you have multiple
1245
- // Backbone views attached to the same DOM element.
1246
- undelegateEvents: function() {
1247
- this.$el.unbind('.delegateEvents' + this.cid);
1248
- },
1249
-
1250
- // Performs the initial configuration of a View with a set of options.
1251
- // Keys with special meaning *(model, collection, id, className)*, are
1252
- // attached directly to the view.
1253
- _configure: function(options) {
1254
- if (this.options) options = _.extend({}, this.options, options);
1255
- for (var i = 0, l = viewOptions.length; i < l; i++) {
1256
- var attr = viewOptions[i];
1257
- if (options[attr]) this[attr] = options[attr];
1258
- }
1259
- this.options = options;
1260
- },
1261
-
1262
- // Ensure that the View has a DOM element to render into.
1263
- // If `this.el` is a string, pass it through `$()`, take the first
1264
- // matching element, and re-assign it to `el`. Otherwise, create
1265
- // an element from the `id`, `className` and `tagName` properties.
1266
- _ensureElement: function() {
1267
- if (!this.el) {
1268
- var attrs = getValue(this, 'attributes') || {};
1269
- if (this.id) attrs.id = this.id;
1270
- if (this.className) attrs['class'] = this.className;
1271
- this.setElement(this.make(this.tagName, attrs), false);
1272
- } else {
1273
- this.setElement(this.el, false);
1800
+ // Some browsers require that `hash` contains a leading #.
1801
+ location.hash = '#' + fragment;
1274
1802
  }
1275
1803
  }
1276
1804
 
1277
1805
  });
1278
1806
 
1279
- // The self-propagating extend function that Backbone classes use.
1280
- var extend = function (protoProps, classProps) {
1281
- var child = inherits(this, protoProps, classProps);
1282
- child.extend = this.extend;
1283
- return child;
1284
- };
1285
-
1286
- // Set up inheritance for the model, collection, and view.
1287
- Model.extend = Collection.extend = Router.extend = View.extend = extend;
1288
-
1289
- // Backbone.sync
1290
- // -------------
1291
-
1292
- // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1293
- var methodMap = {
1294
- 'create': 'POST',
1295
- 'update': 'PUT',
1296
- 'delete': 'DELETE',
1297
- 'read': 'GET'
1298
- };
1299
-
1300
- // Override this function to change the manner in which Backbone persists
1301
- // models to the server. You will be passed the type of request, and the
1302
- // model in question. By default, makes a RESTful Ajax request
1303
- // to the model's `url()`. Some possible customizations could be:
1304
- //
1305
- // * Use `setTimeout` to batch rapid-fire updates into a single request.
1306
- // * Send up the models as XML instead of JSON.
1307
- // * Persist models via WebSockets instead of Ajax.
1308
- //
1309
- // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1310
- // as `POST`, with a `_method` parameter containing the true HTTP method,
1311
- // as well as all requests with the body as `application/x-www-form-urlencoded`
1312
- // instead of `application/json` with the model in a param named `model`.
1313
- // Useful when interfacing with server-side languages like **PHP** that make
1314
- // it difficult to read the body of `PUT` requests.
1315
- Backbone.sync = function(method, model, options) {
1316
- var type = methodMap[method];
1317
-
1318
- // Default options, unless specified.
1319
- options || (options = {});
1320
-
1321
- // Default JSON-request options.
1322
- var params = {type: type, dataType: 'json'};
1323
-
1324
- // Ensure that we have a URL.
1325
- if (!options.url) {
1326
- params.url = getValue(model, 'url') || urlError();
1327
- }
1328
-
1329
- // Ensure that we have the appropriate request data.
1330
- if (!options.data && model && (method == 'create' || method == 'update')) {
1331
- params.contentType = 'application/json';
1332
- params.data = JSON.stringify(model.toJSON());
1333
- }
1334
-
1335
- // For older servers, emulate JSON by encoding the request into an HTML-form.
1336
- if (Backbone.emulateJSON) {
1337
- params.contentType = 'application/x-www-form-urlencoded';
1338
- params.data = params.data ? {model: params.data} : {};
1339
- }
1340
-
1341
- // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1342
- // And an `X-HTTP-Method-Override` header.
1343
- if (Backbone.emulateHTTP) {
1344
- if (type === 'PUT' || type === 'DELETE') {
1345
- if (Backbone.emulateJSON) params.data._method = type;
1346
- params.type = 'POST';
1347
- params.beforeSend = function(xhr) {
1348
- xhr.setRequestHeader('X-HTTP-Method-Override', type);
1349
- };
1350
- }
1351
- }
1352
-
1353
- // Don't process data on a non-GET request.
1354
- if (params.type !== 'GET' && !Backbone.emulateJSON) {
1355
- params.processData = false;
1356
- }
1357
-
1358
- // Make the request, allowing the user to override any Ajax options.
1359
- return $.ajax(_.extend(params, options));
1360
- };
1361
-
1362
- // Wrap an optional error callback with a fallback error event.
1363
- Backbone.wrapError = function(onError, originalModel, options) {
1364
- return function(model, resp) {
1365
- resp = model === originalModel ? resp : model;
1366
- if (onError) {
1367
- onError(originalModel, resp, options);
1368
- } else {
1369
- originalModel.trigger('error', originalModel, resp, options);
1370
- }
1371
- };
1372
- };
1807
+ // Create the default Backbone.history.
1808
+ Backbone.history = new History;
1373
1809
 
1374
1810
  // Helpers
1375
1811
  // -------
1376
1812
 
1377
- // Shared empty constructor function to aid in prototype-chain creation.
1378
- var ctor = function(){};
1379
-
1380
- // Helper function to correctly set up the prototype chain, for subclasses.
1813
+ // Helper function to correctly set up the prototype chain for subclasses.
1381
1814
  // Similar to `goog.inherits`, but uses a hash of prototype properties and
1382
1815
  // class properties to be extended.
1383
- var inherits = function(parent, protoProps, staticProps) {
1816
+ var extend = function(protoProps, staticProps) {
1817
+ var parent = this;
1384
1818
  var child;
1385
1819
 
1386
1820
  // The constructor function for the new subclass is either defined by you
1387
1821
  // (the "constructor" property in your `extend` definition), or defaulted
1388
- // by us to simply call the parent's constructor.
1389
- if (protoProps && protoProps.hasOwnProperty('constructor')) {
1822
+ // by us to simply call the parent constructor.
1823
+ if (protoProps && _.has(protoProps, 'constructor')) {
1390
1824
  child = protoProps.constructor;
1391
1825
  } else {
1392
- child = function(){ parent.apply(this, arguments); };
1826
+ child = function(){ return parent.apply(this, arguments); };
1393
1827
  }
1394
1828
 
1395
- // Inherit class (static) properties from parent.
1396
- _.extend(child, parent);
1829
+ // Add static properties to the constructor function, if supplied.
1830
+ _.extend(child, parent, staticProps);
1397
1831
 
1398
1832
  // Set the prototype chain to inherit from `parent`, without calling
1399
- // `parent`'s constructor function.
1400
- ctor.prototype = parent.prototype;
1401
- child.prototype = new ctor();
1833
+ // `parent` constructor function.
1834
+ var Surrogate = function(){ this.constructor = child; };
1835
+ Surrogate.prototype = parent.prototype;
1836
+ child.prototype = new Surrogate;
1402
1837
 
1403
1838
  // Add prototype properties (instance properties) to the subclass,
1404
1839
  // if supplied.
1405
1840
  if (protoProps) _.extend(child.prototype, protoProps);
1406
1841
 
1407
- // Add static properties to the constructor function, if supplied.
1408
- if (staticProps) _.extend(child, staticProps);
1409
-
1410
- // Correctly set child's `prototype.constructor`.
1411
- child.prototype.constructor = child;
1412
-
1413
- // Set a convenience property in case the parent's prototype is needed later.
1842
+ // Set a convenience property in case the parent's prototype is needed
1843
+ // later.
1414
1844
  child.__super__ = parent.prototype;
1415
1845
 
1416
1846
  return child;
1417
1847
  };
1418
1848
 
1419
- // Helper function to get a value from a Backbone object as a property
1420
- // or as a function.
1421
- var getValue = function(object, prop) {
1422
- if (!(object && object[prop])) return null;
1423
- return _.isFunction(object[prop]) ? object[prop]() : object[prop];
1424
- };
1849
+ // Set up inheritance for the model, collection, router, view and history.
1850
+ Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
1425
1851
 
1426
1852
  // Throw an error when a URL is needed, and none is supplied.
1427
1853
  var urlError = function() {
1428
1854
  throw new Error('A "url" property or function must be specified');
1429
1855
  };
1430
1856
 
1431
- }).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
+ }));