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