appetizer-ui 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/appetizer-ui.gemspec +15 -13
  2. data/lib/appetizer/ui/assets.rb +21 -8
  3. data/lib/appetizer/ui/jasmine/js/jquery-jasmine.js +288 -0
  4. data/lib/appetizer/ui/jasmine/js/spec-runner.coffee +0 -1
  5. data/lib/appetizer/ui/jasmine/views/specs.erb +12 -4
  6. data/lib/appetizer/ui/rake.rb +29 -10
  7. data/lib/appetizer/ui/spec.rb +1 -2
  8. metadata +122 -61
  9. data/lib/appetizer/ui/app/js/appetizer.coffee +0 -15
  10. data/lib/appetizer/ui/app/js/appetizer/core.coffee +0 -20
  11. data/lib/appetizer/ui/app/js/appetizer/model.coffee +0 -1
  12. data/lib/appetizer/ui/app/js/appetizer/router.coffee +0 -70
  13. data/lib/appetizer/ui/app/js/appetizer/view.coffee +0 -139
  14. data/lib/appetizer/ui/app/js/appetizer/xdr.coffee +0 -57
  15. data/lib/appetizer/ui/app/views/client/appetizer/missing.jst.eco +0 -3
  16. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/appetizerExtensions.spec.js +0 -54
  17. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/checkboxConventionBindings.spec.js +0 -110
  18. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/configurableBindingAttributes.spec.js +0 -117
  19. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/configureAllBindingAttributes.spec.js +0 -139
  20. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/customConvention.spec.js +0 -53
  21. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/dataBindConvention.spec.js +0 -151
  22. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/dataBindMultiple.spec.js +0 -36
  23. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/dataBindSubstitutions.spec.js +0 -137
  24. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/globalConfiguraAllBindingAttributes.spec.js +0 -124
  25. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/globalConfigurableBindingAttributes.spec.js +0 -36
  26. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/helpers/SpecHelper.js +0 -4
  27. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/helpers/sample.backbone.app.js +0 -159
  28. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/html5inputConventionBinding.spec.js +0 -142
  29. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/modelUnbinding.spec.js +0 -73
  30. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/noConflict.spec.js +0 -36
  31. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/radioButtonConventionBinding.spec.js +0 -41
  32. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/selectboxConventionBindings.spec.js +0 -60
  33. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/textareaConventionBinding.spec.js +0 -29
  34. data/lib/appetizer/ui/jasmine/js/backbone.modelbinding/textboxConventionBinding.spec.js +0 -66
  35. data/lib/appetizer/ui/vendor/js/backbone.js +0 -1290
  36. data/lib/appetizer/ui/vendor/js/backbone.modelbinding.js +0 -624
  37. data/lib/appetizer/ui/vendor/js/jquery.js +0 -9266
  38. data/lib/appetizer/ui/vendor/js/json2.js +0 -480
  39. data/lib/appetizer/ui/vendor/js/underscore.js +0 -999
  40. data/lib/appetizer/ui/vendor/js/underscore.string.js +0 -480
@@ -1,41 +0,0 @@
1
- describe("radio button convention binding", function(){
2
- beforeEach(function(){
3
- this.model = new AModel({
4
- graduated: "maybe",
5
- us_citizen: false
6
- });
7
- this.view = new AView({model: this.model});
8
- this.view.render();
9
- });
10
-
11
- it("bind view changes to the model's field, by convention of id", function(){
12
- var el = $(this.view.el).find("#graduated_no");
13
- el.attr("checked", "checked");
14
- el.trigger('change');
15
- expect(this.model.get('graduated')).toEqual("no");
16
- });
17
-
18
- it("bind model field changes to the form input, by convention of id", function(){
19
- this.model.set({graduated: "yes"});
20
- var el = this.view.$("#graduated_yes");
21
- var selected = el.attr("checked");
22
-
23
- expect(selected).toBeTruthy();
24
- });
25
-
26
- it("binds the model's value to the form field on render (graduated)", function(){
27
- var el = this.view.$("input[type=radio][name=graduated]:checked");
28
- var selected = el.val();
29
-
30
- expect(selected).toBe("maybe");
31
- });
32
-
33
- it("binds the model's value to the form field on render (us_citizen)", function(){
34
- var el = this.view.$("#us_citizen_false");
35
- expect(el.is(':checked')).toBe(true);
36
- });
37
-
38
- it("binds the view's value to the model, when there is no value in the model", function(){
39
- expect(this.model.get("another_radio")).toBeTruthy();
40
- });
41
- });
@@ -1,60 +0,0 @@
1
- describe("select element convention binding", function(){
2
- beforeEach(function(){
3
- this.model = new AModel({
4
- name: "Ashelia Bailey",
5
- education: "graduate",
6
- age_level: 0,
7
- graduated: "maybe",
8
- us_citizen: false,
9
- drivers_license: true,
10
- motorcycle_license: false,
11
- bio: "my baby girl",
12
- operating_system: "non existent value"
13
- });
14
- this.view = new AView({model: this.model});
15
- this.view.render();
16
- });
17
-
18
- it("bind view changes to the model's field, by convention of id", function(){
19
- var el = this.view.$("#education");
20
- el.val("college");
21
- el.trigger('change');
22
-
23
- expect(this.model.get('education')).toEqual("college");
24
- });
25
-
26
- it("bind model field changes to the form input, by convention of id", function(){
27
- this.model.set({education: "high school"});
28
- var el = this.view.$("#education");
29
- expect(el.val()).toEqual("high school");
30
- });
31
-
32
- it("binds the model's value to the form field on render (education)", function(){
33
- var el = this.view.$("#education");
34
- expect(el.val()).toEqual("graduate");
35
- });
36
-
37
- it("binds the model's value to the form field on render (age_level)", function(){
38
- var el = this.view.$("#age_level");
39
- expect(el.val()).toEqual("0");
40
- });
41
-
42
- it("applies the text of the selection to the model", function(){
43
- var el = this.view.$("#education");
44
- el.val("grade_school");
45
- el.trigger('change');
46
-
47
- expect(this.model.get('education_text')).toEqual("i dun learned at grade skool");
48
- });
49
-
50
- it("updates the model to the selected value when the model is set to a value that doesn't exist, on render", function(){
51
- var el = this.view.$("#operating_system");
52
- var elVal = el.val();
53
-
54
- expect(this.model.get('operating_system')).toEqual(elVal);
55
- });
56
-
57
- it("binds the select box value to the model, when there is no model value, on render", function(){
58
- expect(this.model.get("another_select")).toEqual("pre_selected");
59
- });
60
- });
@@ -1,29 +0,0 @@
1
- describe("text area convention binding", function(){
2
- beforeEach(function(){
3
- this.model = new AModel({
4
- bio: "my biography"
5
- });
6
- this.view = new AView({model: this.model});
7
- this.view.render();
8
- });
9
-
10
- it("bind view changes to the model's field, by convention of id", function(){
11
- var el = this.view.$("#bio");
12
- el.val("my biography");
13
- el.trigger('change');
14
-
15
- expect(this.model.get('bio')).toEqual("my biography");
16
- });
17
-
18
- it("bind model field changes to the form input, by convention of id", function(){
19
- this.model.set({bio: "a modified biogrpahy"});
20
- var el = this.view.$("#bio");
21
- expect(el.val()).toEqual("a modified biogrpahy");
22
- });
23
-
24
- it("binds the model's value to the form field on render", function(){
25
- var el = this.view.$("#bio");
26
- expect(el.val()).toEqual("my biography");
27
- });
28
-
29
- });
@@ -1,66 +0,0 @@
1
- describe("textbox convention bindings", function(){
2
- beforeEach(function(){
3
- this.model = new AModel({
4
- name: "Ashelia Bailey",
5
- noType: 'there is no type'
6
- });
7
- this.view = new AView({model: this.model});
8
- });
9
-
10
- describe("text element binding", function(){
11
- beforeEach(function(){
12
- this.view.render();
13
- this.el = this.view.$("#name");
14
- });
15
-
16
- it("bind view changes to the model's field, by convention of id", function(){
17
- this.el.val("Derick Bailey");
18
- this.el.trigger('change');
19
-
20
- expect(this.model.get('name')).toEqual("Derick Bailey");
21
- });
22
-
23
- it("bind model field changes to the form input, by convention of id", function(){
24
- this.model.set({name: "Ian Bailey"});
25
- expect(this.el.val()).toEqual("Ian Bailey");
26
- });
27
-
28
- it("binds the model's value to the form field on render", function(){
29
- expect(this.el.val()).toEqual("Ashelia Bailey");
30
- });
31
- });
32
-
33
- describe("when the form field has a value but the model does not", function(){
34
- beforeEach(function(){
35
- this.view.render();
36
- var el = this.view.$("#prefilled_name");
37
- });
38
-
39
- it("binds the form field's value to the model, on render", function(){
40
- expect(this.model.get("prefilled_name")).toBe("a name");
41
- });
42
- });
43
-
44
- describe("input with no type specified, binding", function(){
45
- beforeEach(function(){
46
- this.view.render();
47
- this.el = this.view.$("#noType");
48
- });
49
-
50
- it("bind view changes to the model's field, by convention of id", function(){
51
- this.el.val("something changed");
52
- this.el.trigger('change');
53
-
54
- expect(this.model.get('noType')).toEqual("something changed");
55
- });
56
-
57
- it("bind model field changes to the form input, by convention of id", function(){
58
- this.model.set({noType: "Ian Bailey"});
59
- expect(this.el.val()).toEqual("Ian Bailey");
60
- });
61
-
62
- it("binds the model's value to the form field on render", function(){
63
- expect(this.el.val()).toEqual("there is no type");
64
- });
65
- });
66
- });
@@ -1,1290 +0,0 @@
1
- // Backbone.js 0.9.1
2
-
3
- // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
4
- // Backbone may be freely distributed under the MIT license.
5
- // For all details and documentation:
6
- // http://backbonejs.org
7
-
8
- (function(){
9
-
10
- // Initial Setup
11
- // -------------
12
-
13
- // Save a reference to the global object (`window` in the browser, `global`
14
- // on the server).
15
- var root = this;
16
-
17
- // Save the previous value of the `Backbone` variable, so that it can be
18
- // restored later on, if `noConflict` is used.
19
- var previousBackbone = root.Backbone;
20
-
21
- // Create a local reference to slice/splice.
22
- var slice = Array.prototype.slice;
23
- var splice = Array.prototype.splice;
24
-
25
- // The top-level namespace. All public Backbone classes and modules will
26
- // be attached to this. Exported for both CommonJS and the browser.
27
- var Backbone;
28
- if (typeof exports !== 'undefined') {
29
- Backbone = exports;
30
- } else {
31
- Backbone = root.Backbone = {};
32
- }
33
-
34
- // Current version of the library. Keep in sync with `package.json`.
35
- Backbone.VERSION = '0.9.1';
36
-
37
- // Require Underscore, if we're on the server, and it's not already present.
38
- var _ = root._;
39
- if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
40
-
41
- // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
42
- var $ = root.jQuery || root.Zepto || root.ender;
43
-
44
- // Set the JavaScript library that will be used for DOM manipulation and
45
- // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
46
- // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
47
- // alternate JavaScript library (or a mock library for testing your views
48
- // outside of a browser).
49
- Backbone.setDomLibrary = function(lib) {
50
- $ = lib;
51
- };
52
-
53
- // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
54
- // to its previous owner. Returns a reference to this Backbone object.
55
- Backbone.noConflict = function() {
56
- root.Backbone = previousBackbone;
57
- return this;
58
- };
59
-
60
- // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
61
- // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
62
- // set a `X-Http-Method-Override` header.
63
- Backbone.emulateHTTP = false;
64
-
65
- // Turn on `emulateJSON` to support legacy servers that can't deal with direct
66
- // `application/json` requests ... will encode the body as
67
- // `application/x-www-form-urlencoded` instead and will send the model in a
68
- // form param named `model`.
69
- Backbone.emulateJSON = false;
70
-
71
- // Backbone.Events
72
- // -----------------
73
-
74
- // A module that can be mixed in to *any object* in order to provide it with
75
- // custom events. You may bind with `on` or remove with `off` callback functions
76
- // to an event; trigger`-ing an event fires all callbacks in succession.
77
- //
78
- // var object = {};
79
- // _.extend(object, Backbone.Events);
80
- // object.on('expand', function(){ alert('expanded'); });
81
- // object.trigger('expand');
82
- //
83
- Backbone.Events = {
84
-
85
- // Bind an event, specified by a string name, `ev`, to a `callback`
86
- // function. Passing `"all"` will bind the callback to all events fired.
87
- on: function(events, callback, context) {
88
- var ev;
89
- events = events.split(/\s+/);
90
- var calls = this._callbacks || (this._callbacks = {});
91
- while (ev = events.shift()) {
92
- // Create an immutable callback list, allowing traversal during
93
- // modification. The tail is an empty object that will always be used
94
- // as the next node.
95
- var list = calls[ev] || (calls[ev] = {});
96
- var tail = list.tail || (list.tail = list.next = {});
97
- tail.callback = callback;
98
- tail.context = context;
99
- list.tail = tail.next = {};
100
- }
101
- return this;
102
- },
103
-
104
- // Remove one or many callbacks. If `context` is null, removes all callbacks
105
- // with that function. If `callback` is null, removes all callbacks for the
106
- // event. If `ev` is null, removes all bound callbacks for all events.
107
- off: function(events, callback, context) {
108
- var ev, calls, node;
109
- if (!events) {
110
- delete this._callbacks;
111
- } else if (calls = this._callbacks) {
112
- events = events.split(/\s+/);
113
- while (ev = events.shift()) {
114
- node = calls[ev];
115
- delete calls[ev];
116
- if (!callback || !node) continue;
117
- // Create a new list, omitting the indicated event/context pairs.
118
- while ((node = node.next) && node.next) {
119
- if (node.callback === callback &&
120
- (!context || node.context === context)) continue;
121
- this.on(ev, node.callback, node.context);
122
- }
123
- }
124
- }
125
- return this;
126
- },
127
-
128
- // Trigger an event, firing all bound callbacks. Callbacks are passed the
129
- // same arguments as `trigger` is, apart from the event name.
130
- // Listening for `"all"` passes the true event name as the first argument.
131
- trigger: function(events) {
132
- var event, node, calls, tail, args, all, rest;
133
- if (!(calls = this._callbacks)) return this;
134
- all = calls['all'];
135
- (events = events.split(/\s+/)).push(null);
136
- // Save references to the current heads & tails.
137
- while (event = events.shift()) {
138
- if (all) events.push({next: all.next, tail: all.tail, event: event});
139
- if (!(node = calls[event])) continue;
140
- events.push({next: node.next, tail: node.tail});
141
- }
142
- // Traverse each list, stopping when the saved tail is reached.
143
- rest = slice.call(arguments, 1);
144
- while (node = events.pop()) {
145
- tail = node.tail;
146
- args = node.event ? [node.event].concat(rest) : rest;
147
- while ((node = node.next) !== tail) {
148
- node.callback.apply(node.context || this, args);
149
- }
150
- }
151
- return this;
152
- }
153
-
154
- };
155
-
156
- // Aliases for backwards compatibility.
157
- Backbone.Events.bind = Backbone.Events.on;
158
- Backbone.Events.unbind = Backbone.Events.off;
159
-
160
- // Backbone.Model
161
- // --------------
162
-
163
- // Create a new model, with defined attributes. A client id (`cid`)
164
- // is automatically generated and assigned for you.
165
- Backbone.Model = function(attributes, options) {
166
- var defaults;
167
- attributes || (attributes = {});
168
- if (options && options.parse) attributes = this.parse(attributes);
169
- if (defaults = getValue(this, 'defaults')) {
170
- attributes = _.extend({}, defaults, attributes);
171
- }
172
- if (options && options.collection) this.collection = options.collection;
173
- this.attributes = {};
174
- this._escapedAttributes = {};
175
- this.cid = _.uniqueId('c');
176
- if (!this.set(attributes, {silent: true})) {
177
- throw new Error("Can't create an invalid model");
178
- }
179
- delete this._changed;
180
- this._previousAttributes = _.clone(this.attributes);
181
- this.initialize.apply(this, arguments);
182
- };
183
-
184
- // Attach all inheritable methods to the Model prototype.
185
- _.extend(Backbone.Model.prototype, Backbone.Events, {
186
-
187
- // The default name for the JSON `id` attribute is `"id"`. MongoDB and
188
- // CouchDB users may want to set this to `"_id"`.
189
- idAttribute: 'id',
190
-
191
- // Initialize is an empty function by default. Override it with your own
192
- // initialization logic.
193
- initialize: function(){},
194
-
195
- // Return a copy of the model's `attributes` object.
196
- toJSON: function() {
197
- return _.clone(this.attributes);
198
- },
199
-
200
- // Get the value of an attribute.
201
- get: function(attr) {
202
- return this.attributes[attr];
203
- },
204
-
205
- // Get the HTML-escaped value of an attribute.
206
- escape: function(attr) {
207
- var html;
208
- if (html = this._escapedAttributes[attr]) return html;
209
- var val = this.attributes[attr];
210
- return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
211
- },
212
-
213
- // Returns `true` if the attribute contains a value that is not null
214
- // or undefined.
215
- has: function(attr) {
216
- return this.attributes[attr] != null;
217
- },
218
-
219
- // Set a hash of model attributes on the object, firing `"change"` unless
220
- // you choose to silence it.
221
- set: function(key, value, options) {
222
- var attrs, attr, val;
223
- if (_.isObject(key) || key == null) {
224
- attrs = key;
225
- options = value;
226
- } else {
227
- attrs = {};
228
- attrs[key] = value;
229
- }
230
-
231
- // Extract attributes and options.
232
- options || (options = {});
233
- if (!attrs) return this;
234
- if (attrs instanceof Backbone.Model) attrs = attrs.attributes;
235
- if (options.unset) for (attr in attrs) attrs[attr] = void 0;
236
-
237
- // Run validation.
238
- if (!this._validate(attrs, options)) return false;
239
-
240
- // Check for changes of `id`.
241
- if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
242
-
243
- var now = this.attributes;
244
- var escaped = this._escapedAttributes;
245
- var prev = this._previousAttributes || {};
246
- var alreadySetting = this._setting;
247
- this._changed || (this._changed = {});
248
- this._setting = true;
249
-
250
- // Update attributes.
251
- for (attr in attrs) {
252
- val = attrs[attr];
253
- if (!_.isEqual(now[attr], val)) delete escaped[attr];
254
- options.unset ? delete now[attr] : now[attr] = val;
255
- if (this._changing && !_.isEqual(this._changed[attr], val)) {
256
- this.trigger('change:' + attr, this, val, options);
257
- this._moreChanges = true;
258
- }
259
- delete this._changed[attr];
260
- if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
261
- this._changed[attr] = val;
262
- }
263
- }
264
-
265
- // Fire the `"change"` events, if the model has been changed.
266
- if (!alreadySetting) {
267
- if (!options.silent && this.hasChanged()) this.change(options);
268
- this._setting = false;
269
- }
270
- return this;
271
- },
272
-
273
- // Remove an attribute from the model, firing `"change"` unless you choose
274
- // to silence it. `unset` is a noop if the attribute doesn't exist.
275
- unset: function(attr, options) {
276
- (options || (options = {})).unset = true;
277
- return this.set(attr, null, options);
278
- },
279
-
280
- // Clear all attributes on the model, firing `"change"` unless you choose
281
- // to silence it.
282
- clear: function(options) {
283
- (options || (options = {})).unset = true;
284
- return this.set(_.clone(this.attributes), options);
285
- },
286
-
287
- // Fetch the model from the server. If the server's representation of the
288
- // model differs from its current attributes, they will be overriden,
289
- // triggering a `"change"` event.
290
- fetch: function(options) {
291
- options = options ? _.clone(options) : {};
292
- var model = this;
293
- var success = options.success;
294
- options.success = function(resp, status, xhr) {
295
- if (!model.set(model.parse(resp, xhr), options)) return false;
296
- if (success) success(model, resp);
297
- };
298
- options.error = Backbone.wrapError(options.error, model, options);
299
- return (this.sync || Backbone.sync).call(this, 'read', this, options);
300
- },
301
-
302
- // Set a hash of model attributes, and sync the model to the server.
303
- // If the server returns an attributes hash that differs, the model's
304
- // state will be `set` again.
305
- save: function(key, value, options) {
306
- var attrs, current;
307
- if (_.isObject(key) || key == null) {
308
- attrs = key;
309
- options = value;
310
- } else {
311
- attrs = {};
312
- attrs[key] = value;
313
- }
314
-
315
- options = options ? _.clone(options) : {};
316
- if (options.wait) current = _.clone(this.attributes);
317
- var silentOptions = _.extend({}, options, {silent: true});
318
- if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
319
- return false;
320
- }
321
- var model = this;
322
- var success = options.success;
323
- options.success = function(resp, status, xhr) {
324
- var serverAttrs = model.parse(resp, xhr);
325
- if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
326
- if (!model.set(serverAttrs, options)) return false;
327
- if (success) {
328
- success(model, resp);
329
- } else {
330
- model.trigger('sync', model, resp, options);
331
- }
332
- };
333
- options.error = Backbone.wrapError(options.error, model, options);
334
- var method = this.isNew() ? 'create' : 'update';
335
- var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
336
- if (options.wait) this.set(current, silentOptions);
337
- return xhr;
338
- },
339
-
340
- // Destroy this model on the server if it was already persisted.
341
- // Optimistically removes the model from its collection, if it has one.
342
- // If `wait: true` is passed, waits for the server to respond before removal.
343
- destroy: function(options) {
344
- options = options ? _.clone(options) : {};
345
- var model = this;
346
- var success = options.success;
347
-
348
- var triggerDestroy = function() {
349
- model.trigger('destroy', model, model.collection, options);
350
- };
351
-
352
- if (this.isNew()) return triggerDestroy();
353
- options.success = function(resp) {
354
- if (options.wait) triggerDestroy();
355
- if (success) {
356
- success(model, resp);
357
- } else {
358
- model.trigger('sync', model, resp, options);
359
- }
360
- };
361
- options.error = Backbone.wrapError(options.error, model, options);
362
- var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
363
- if (!options.wait) triggerDestroy();
364
- return xhr;
365
- },
366
-
367
- // Default URL for the model's representation on the server -- if you're
368
- // using Backbone's restful methods, override this to change the endpoint
369
- // that will be called.
370
- url: function() {
371
- var base = getValue(this.collection, 'url') || getValue(this, 'urlRoot') || urlError();
372
- if (this.isNew()) return base;
373
- return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
374
- },
375
-
376
- // **parse** converts a response into the hash of attributes to be `set` on
377
- // the model. The default implementation is just to pass the response along.
378
- parse: function(resp, xhr) {
379
- return resp;
380
- },
381
-
382
- // Create a new model with identical attributes to this one.
383
- clone: function() {
384
- return new this.constructor(this.attributes);
385
- },
386
-
387
- // A model is new if it has never been saved to the server, and lacks an id.
388
- isNew: function() {
389
- return this.id == null;
390
- },
391
-
392
- // Call this method to manually fire a `"change"` event for this model and
393
- // a `"change:attribute"` event for each changed attribute.
394
- // Calling this will cause all objects observing the model to update.
395
- change: function(options) {
396
- if (this._changing || !this.hasChanged()) return this;
397
- this._changing = true;
398
- this._moreChanges = true;
399
- for (var attr in this._changed) {
400
- this.trigger('change:' + attr, this, this._changed[attr], options);
401
- }
402
- while (this._moreChanges) {
403
- this._moreChanges = false;
404
- this.trigger('change', this, options);
405
- }
406
- this._previousAttributes = _.clone(this.attributes);
407
- delete this._changed;
408
- this._changing = false;
409
- return this;
410
- },
411
-
412
- // Determine if the model has changed since the last `"change"` event.
413
- // If you specify an attribute name, determine if that attribute has changed.
414
- hasChanged: function(attr) {
415
- if (!arguments.length) return !_.isEmpty(this._changed);
416
- return this._changed && _.has(this._changed, attr);
417
- },
418
-
419
- // Return an object containing all the attributes that have changed, or
420
- // false if there are no changed attributes. Useful for determining what
421
- // parts of a view need to be updated and/or what attributes need to be
422
- // persisted to the server. Unset attributes will be set to undefined.
423
- // You can also pass an attributes object to diff against the model,
424
- // determining if there *would be* a change.
425
- changedAttributes: function(diff) {
426
- if (!diff) return this.hasChanged() ? _.clone(this._changed) : false;
427
- var val, changed = false, old = this._previousAttributes;
428
- for (var attr in diff) {
429
- if (_.isEqual(old[attr], (val = diff[attr]))) continue;
430
- (changed || (changed = {}))[attr] = val;
431
- }
432
- return changed;
433
- },
434
-
435
- // Get the previous value of an attribute, recorded at the time the last
436
- // `"change"` event was fired.
437
- previous: function(attr) {
438
- if (!arguments.length || !this._previousAttributes) return null;
439
- return this._previousAttributes[attr];
440
- },
441
-
442
- // Get all of the attributes of the model at the time of the previous
443
- // `"change"` event.
444
- previousAttributes: function() {
445
- return _.clone(this._previousAttributes);
446
- },
447
-
448
- // Check if the model is currently in a valid state. It's only possible to
449
- // get into an *invalid* state if you're using silent changes.
450
- isValid: function() {
451
- return !this.validate(this.attributes);
452
- },
453
-
454
- // Run validation against a set of incoming attributes, returning `true`
455
- // if all is well. If a specific `error` callback has been passed,
456
- // call that instead of firing the general `"error"` event.
457
- _validate: function(attrs, options) {
458
- if (options.silent || !this.validate) return true;
459
- attrs = _.extend({}, this.attributes, attrs);
460
- var error = this.validate(attrs, options);
461
- if (!error) return true;
462
- if (options && options.error) {
463
- options.error(this, error, options);
464
- } else {
465
- this.trigger('error', this, error, options);
466
- }
467
- return false;
468
- }
469
-
470
- });
471
-
472
- // Backbone.Collection
473
- // -------------------
474
-
475
- // Provides a standard collection class for our sets of models, ordered
476
- // or unordered. If a `comparator` is specified, the Collection will maintain
477
- // its models in sort order, as they're added and removed.
478
- Backbone.Collection = function(models, options) {
479
- options || (options = {});
480
- if (options.comparator) this.comparator = options.comparator;
481
- this._reset();
482
- this.initialize.apply(this, arguments);
483
- if (models) this.reset(models, {silent: true, parse: options.parse});
484
- };
485
-
486
- // Define the Collection's inheritable methods.
487
- _.extend(Backbone.Collection.prototype, Backbone.Events, {
488
-
489
- // The default model for a collection is just a **Backbone.Model**.
490
- // This should be overridden in most cases.
491
- model: Backbone.Model,
492
-
493
- // Initialize is an empty function by default. Override it with your own
494
- // initialization logic.
495
- initialize: function(){},
496
-
497
- // The JSON representation of a Collection is an array of the
498
- // models' attributes.
499
- toJSON: function() {
500
- return this.map(function(model){ return model.toJSON(); });
501
- },
502
-
503
- // Add a model, or list of models to the set. Pass **silent** to avoid
504
- // firing the `add` event for every new model.
505
- add: function(models, options) {
506
- var i, index, length, model, cid, id, cids = {}, ids = {};
507
- options || (options = {});
508
- models = _.isArray(models) ? models.slice() : [models];
509
-
510
- // Begin by turning bare objects into model references, and preventing
511
- // invalid models or duplicate models from being added.
512
- for (i = 0, length = models.length; i < length; i++) {
513
- if (!(model = models[i] = this._prepareModel(models[i], options))) {
514
- throw new Error("Can't add an invalid model to a collection");
515
- }
516
- if (cids[cid = model.cid] || this._byCid[cid] ||
517
- (((id = model.id) != null) && (ids[id] || this._byId[id]))) {
518
- throw new Error("Can't add the same model to a collection twice");
519
- }
520
- cids[cid] = ids[id] = model;
521
- }
522
-
523
- // Listen to added models' events, and index models for lookup by
524
- // `id` and by `cid`.
525
- for (i = 0; i < length; i++) {
526
- (model = models[i]).on('all', this._onModelEvent, this);
527
- this._byCid[model.cid] = model;
528
- if (model.id != null) this._byId[model.id] = model;
529
- }
530
-
531
- // Insert models into the collection, re-sorting if needed, and triggering
532
- // `add` events unless silenced.
533
- this.length += length;
534
- index = options.at != null ? options.at : this.models.length;
535
- splice.apply(this.models, [index, 0].concat(models));
536
- if (this.comparator) this.sort({silent: true});
537
- if (options.silent) return this;
538
- for (i = 0, length = this.models.length; i < length; i++) {
539
- if (!cids[(model = this.models[i]).cid]) continue;
540
- options.index = i;
541
- model.trigger('add', model, this, options);
542
- }
543
- return this;
544
- },
545
-
546
- // Remove a model, or a list of models from the set. Pass silent to avoid
547
- // firing the `remove` event for every model removed.
548
- remove: function(models, options) {
549
- var i, l, index, model;
550
- options || (options = {});
551
- models = _.isArray(models) ? models.slice() : [models];
552
- for (i = 0, l = models.length; i < l; i++) {
553
- model = this.getByCid(models[i]) || this.get(models[i]);
554
- if (!model) continue;
555
- delete this._byId[model.id];
556
- delete this._byCid[model.cid];
557
- index = this.indexOf(model);
558
- this.models.splice(index, 1);
559
- this.length--;
560
- if (!options.silent) {
561
- options.index = index;
562
- model.trigger('remove', model, this, options);
563
- }
564
- this._removeReference(model);
565
- }
566
- return this;
567
- },
568
-
569
- // Get a model from the set by id.
570
- get: function(id) {
571
- if (id == null) return null;
572
- return this._byId[id.id != null ? id.id : id];
573
- },
574
-
575
- // Get a model from the set by client id.
576
- getByCid: function(cid) {
577
- return cid && this._byCid[cid.cid || cid];
578
- },
579
-
580
- // Get the model at the given index.
581
- at: function(index) {
582
- return this.models[index];
583
- },
584
-
585
- // Force the collection to re-sort itself. You don't need to call this under
586
- // normal circumstances, as the set will maintain sort order as each item
587
- // is added.
588
- sort: function(options) {
589
- options || (options = {});
590
- if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
591
- var boundComparator = _.bind(this.comparator, this);
592
- if (this.comparator.length == 1) {
593
- this.models = this.sortBy(boundComparator);
594
- } else {
595
- this.models.sort(boundComparator);
596
- }
597
- if (!options.silent) this.trigger('reset', this, options);
598
- return this;
599
- },
600
-
601
- // Pluck an attribute from each model in the collection.
602
- pluck: function(attr) {
603
- return _.map(this.models, function(model){ return model.get(attr); });
604
- },
605
-
606
- // When you have more items than you want to add or remove individually,
607
- // you can reset the entire set with a new list of models, without firing
608
- // any `add` or `remove` events. Fires `reset` when finished.
609
- reset: function(models, options) {
610
- models || (models = []);
611
- options || (options = {});
612
- for (var i = 0, l = this.models.length; i < l; i++) {
613
- this._removeReference(this.models[i]);
614
- }
615
- this._reset();
616
- this.add(models, {silent: true, parse: options.parse});
617
- if (!options.silent) this.trigger('reset', this, options);
618
- return this;
619
- },
620
-
621
- // Fetch the default set of models for this collection, resetting the
622
- // collection when they arrive. If `add: true` is passed, appends the
623
- // models to the collection instead of resetting.
624
- fetch: function(options) {
625
- options = options ? _.clone(options) : {};
626
- if (options.parse === undefined) options.parse = true;
627
- var collection = this;
628
- var success = options.success;
629
- options.success = function(resp, status, xhr) {
630
- collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
631
- if (success) success(collection, resp);
632
- };
633
- options.error = Backbone.wrapError(options.error, collection, options);
634
- return (this.sync || Backbone.sync).call(this, 'read', this, options);
635
- },
636
-
637
- // Create a new instance of a model in this collection. Add the model to the
638
- // collection immediately, unless `wait: true` is passed, in which case we
639
- // wait for the server to agree.
640
- create: function(model, options) {
641
- var coll = this;
642
- options = options ? _.clone(options) : {};
643
- model = this._prepareModel(model, options);
644
- if (!model) return false;
645
- if (!options.wait) coll.add(model, options);
646
- var success = options.success;
647
- options.success = function(nextModel, resp, xhr) {
648
- if (options.wait) coll.add(nextModel, options);
649
- if (success) {
650
- success(nextModel, resp);
651
- } else {
652
- nextModel.trigger('sync', model, resp, options);
653
- }
654
- };
655
- model.save(null, options);
656
- return model;
657
- },
658
-
659
- // **parse** converts a response into a list of models to be added to the
660
- // collection. The default implementation is just to pass it through.
661
- parse: function(resp, xhr) {
662
- return resp;
663
- },
664
-
665
- // Proxy to _'s chain. Can't be proxied the same way the rest of the
666
- // underscore methods are proxied because it relies on the underscore
667
- // constructor.
668
- chain: function () {
669
- return _(this.models).chain();
670
- },
671
-
672
- // Reset all internal state. Called when the collection is reset.
673
- _reset: function(options) {
674
- this.length = 0;
675
- this.models = [];
676
- this._byId = {};
677
- this._byCid = {};
678
- },
679
-
680
- // Prepare a model or hash of attributes to be added to this collection.
681
- _prepareModel: function(model, options) {
682
- if (!(model instanceof Backbone.Model)) {
683
- var attrs = model;
684
- options.collection = this;
685
- model = new this.model(attrs, options);
686
- if (!model._validate(model.attributes, options)) model = false;
687
- } else if (!model.collection) {
688
- model.collection = this;
689
- }
690
- return model;
691
- },
692
-
693
- // Internal method to remove a model's ties to a collection.
694
- _removeReference: function(model) {
695
- if (this == model.collection) {
696
- delete model.collection;
697
- }
698
- model.off('all', this._onModelEvent, this);
699
- },
700
-
701
- // Internal method called every time a model in the set fires an event.
702
- // Sets need to update their indexes when models change ids. All other
703
- // events simply proxy through. "add" and "remove" events that originate
704
- // in other collections are ignored.
705
- _onModelEvent: function(ev, model, collection, options) {
706
- if ((ev == 'add' || ev == 'remove') && collection != this) return;
707
- if (ev == 'destroy') {
708
- this.remove(model, options);
709
- }
710
- if (model && ev === 'change:' + model.idAttribute) {
711
- delete this._byId[model.previous(model.idAttribute)];
712
- this._byId[model.id] = model;
713
- }
714
- this.trigger.apply(this, arguments);
715
- }
716
-
717
- });
718
-
719
- // Underscore methods that we want to implement on the Collection.
720
- var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
721
- 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
722
- 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
723
- 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
724
- 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
725
-
726
- // Mix in each Underscore method as a proxy to `Collection#models`.
727
- _.each(methods, function(method) {
728
- Backbone.Collection.prototype[method] = function() {
729
- return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
730
- };
731
- });
732
-
733
- // Backbone.Router
734
- // -------------------
735
-
736
- // Routers map faux-URLs to actions, and fire events when routes are
737
- // matched. Creating a new one sets its `routes` hash, if not set statically.
738
- Backbone.Router = function(options) {
739
- options || (options = {});
740
- if (options.routes) this.routes = options.routes;
741
- this._bindRoutes();
742
- this.initialize.apply(this, arguments);
743
- };
744
-
745
- // Cached regular expressions for matching named param parts and splatted
746
- // parts of route strings.
747
- var namedParam = /:\w+/g;
748
- var splatParam = /\*\w+/g;
749
- var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
750
-
751
- // Set up all inheritable **Backbone.Router** properties and methods.
752
- _.extend(Backbone.Router.prototype, Backbone.Events, {
753
-
754
- // Initialize is an empty function by default. Override it with your own
755
- // initialization logic.
756
- initialize: function(){},
757
-
758
- // Manually bind a single named route to a callback. For example:
759
- //
760
- // this.route('search/:query/p:num', 'search', function(query, num) {
761
- // ...
762
- // });
763
- //
764
- route: function(route, name, callback) {
765
- Backbone.history || (Backbone.history = new Backbone.History);
766
- if (!_.isRegExp(route)) route = this._routeToRegExp(route);
767
- if (!callback) callback = this[name];
768
- Backbone.history.route(route, _.bind(function(fragment) {
769
- var args = this._extractParameters(route, fragment);
770
- callback && callback.apply(this, args);
771
- this.trigger.apply(this, ['route:' + name].concat(args));
772
- Backbone.history.trigger('route', this, name, args);
773
- }, this));
774
- return this;
775
- },
776
-
777
- // Simple proxy to `Backbone.history` to save a fragment into the history.
778
- navigate: function(fragment, options) {
779
- Backbone.history.navigate(fragment, options);
780
- },
781
-
782
- // Bind all defined routes to `Backbone.history`. We have to reverse the
783
- // order of the routes here to support behavior where the most general
784
- // routes can be defined at the bottom of the route map.
785
- _bindRoutes: function() {
786
- if (!this.routes) return;
787
- var routes = [];
788
- for (var route in this.routes) {
789
- routes.unshift([route, this.routes[route]]);
790
- }
791
- for (var i = 0, l = routes.length; i < l; i++) {
792
- this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
793
- }
794
- },
795
-
796
- // Convert a route string into a regular expression, suitable for matching
797
- // against the current location hash.
798
- _routeToRegExp: function(route) {
799
- route = route.replace(escapeRegExp, '\\$&')
800
- .replace(namedParam, '([^\/]+)')
801
- .replace(splatParam, '(.*?)');
802
- return new RegExp('^' + route + '$');
803
- },
804
-
805
- // Given a route, and a URL fragment that it matches, return the array of
806
- // extracted parameters.
807
- _extractParameters: function(route, fragment) {
808
- return route.exec(fragment).slice(1);
809
- }
810
-
811
- });
812
-
813
- // Backbone.History
814
- // ----------------
815
-
816
- // Handles cross-browser history management, based on URL fragments. If the
817
- // browser does not support `onhashchange`, falls back to polling.
818
- Backbone.History = function() {
819
- this.handlers = [];
820
- _.bindAll(this, 'checkUrl');
821
- };
822
-
823
- // Cached regex for cleaning leading hashes and slashes .
824
- var routeStripper = /^[#\/]/;
825
-
826
- // Cached regex for detecting MSIE.
827
- var isExplorer = /msie [\w.]+/;
828
-
829
- // Has the history handling already been started?
830
- var historyStarted = false;
831
-
832
- // Set up all inheritable **Backbone.History** properties and methods.
833
- _.extend(Backbone.History.prototype, Backbone.Events, {
834
-
835
- // The default interval to poll for hash changes, if necessary, is
836
- // twenty times a second.
837
- interval: 50,
838
-
839
- // Get the cross-browser normalized URL fragment, either from the URL,
840
- // the hash, or the override.
841
- getFragment: function(fragment, forcePushState) {
842
- if (fragment == null) {
843
- if (this._hasPushState || forcePushState) {
844
- fragment = window.location.pathname;
845
- var search = window.location.search;
846
- if (search) fragment += search;
847
- } else {
848
- fragment = window.location.hash;
849
- }
850
- }
851
- fragment = decodeURIComponent(fragment);
852
- if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
853
- return fragment.replace(routeStripper, '');
854
- },
855
-
856
- // Start the hash change handling, returning `true` if the current URL matches
857
- // an existing route, and `false` otherwise.
858
- start: function(options) {
859
-
860
- // Figure out the initial configuration. Do we need an iframe?
861
- // Is pushState desired ... is it available?
862
- if (historyStarted) throw new Error("Backbone.history has already been started");
863
- this.options = _.extend({}, {root: '/'}, this.options, options);
864
- this._wantsHashChange = this.options.hashChange !== false;
865
- this._wantsPushState = !!this.options.pushState;
866
- this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
867
- var fragment = this.getFragment();
868
- var docMode = document.documentMode;
869
- var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
870
- if (oldIE) {
871
- this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
872
- this.navigate(fragment);
873
- }
874
-
875
- // Depending on whether we're using pushState or hashes, and whether
876
- // 'onhashchange' is supported, determine how we check the URL state.
877
- if (this._hasPushState) {
878
- $(window).bind('popstate', this.checkUrl);
879
- } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
880
- $(window).bind('hashchange', this.checkUrl);
881
- } else if (this._wantsHashChange) {
882
- this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
883
- }
884
-
885
- // Determine if we need to change the base url, for a pushState link
886
- // opened by a non-pushState browser.
887
- this.fragment = fragment;
888
- historyStarted = true;
889
- var loc = window.location;
890
- var atRoot = loc.pathname == this.options.root;
891
-
892
- // If we've started off with a route from a `pushState`-enabled browser,
893
- // but we're currently in a browser that doesn't support it...
894
- if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
895
- this.fragment = this.getFragment(null, true);
896
- window.location.replace(this.options.root + '#' + this.fragment);
897
- // Return immediately as browser will do redirect to new url
898
- return true;
899
-
900
- // Or if we've started out with a hash-based route, but we're currently
901
- // in a browser where it could be `pushState`-based instead...
902
- } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
903
- this.fragment = loc.hash.replace(routeStripper, '');
904
- window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
905
- }
906
-
907
- if (!this.options.silent) {
908
- return this.loadUrl();
909
- }
910
- },
911
-
912
- // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
913
- // but possibly useful for unit testing Routers.
914
- stop: function() {
915
- $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
916
- clearInterval(this._checkUrlInterval);
917
- historyStarted = false;
918
- },
919
-
920
- // Add a route to be tested when the fragment changes. Routes added later
921
- // may override previous routes.
922
- route: function(route, callback) {
923
- this.handlers.unshift({route: route, callback: callback});
924
- },
925
-
926
- // Checks the current URL to see if it has changed, and if it has,
927
- // calls `loadUrl`, normalizing across the hidden iframe.
928
- checkUrl: function(e) {
929
- var current = this.getFragment();
930
- if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
931
- if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
932
- if (this.iframe) this.navigate(current);
933
- this.loadUrl() || this.loadUrl(window.location.hash);
934
- },
935
-
936
- // Attempt to load the current URL fragment. If a route succeeds with a
937
- // match, returns `true`. If no defined routes matches the fragment,
938
- // returns `false`.
939
- loadUrl: function(fragmentOverride) {
940
- var fragment = this.fragment = this.getFragment(fragmentOverride);
941
- var matched = _.any(this.handlers, function(handler) {
942
- if (handler.route.test(fragment)) {
943
- handler.callback(fragment);
944
- return true;
945
- }
946
- });
947
- return matched;
948
- },
949
-
950
- // Save a fragment into the hash history, or replace the URL state if the
951
- // 'replace' option is passed. You are responsible for properly URL-encoding
952
- // the fragment in advance.
953
- //
954
- // The options object can contain `trigger: true` if you wish to have the
955
- // route callback be fired (not usually desirable), or `replace: true`, if
956
- // you which to modify the current URL without adding an entry to the history.
957
- navigate: function(fragment, options) {
958
- if (!historyStarted) return false;
959
- if (!options || options === true) options = {trigger: options};
960
- var frag = (fragment || '').replace(routeStripper, '');
961
- if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
962
-
963
- // If pushState is available, we use it to set the fragment as a real URL.
964
- if (this._hasPushState) {
965
- if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
966
- this.fragment = frag;
967
- window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
968
-
969
- // If hash changes haven't been explicitly disabled, update the hash
970
- // fragment to store history.
971
- } else if (this._wantsHashChange) {
972
- this.fragment = frag;
973
- this._updateHash(window.location, frag, options.replace);
974
- if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
975
- // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
976
- // When replace is true, we don't want this.
977
- if(!options.replace) this.iframe.document.open().close();
978
- this._updateHash(this.iframe.location, frag, options.replace);
979
- }
980
-
981
- // If you've told us that you explicitly don't want fallback hashchange-
982
- // based history, then `navigate` becomes a page refresh.
983
- } else {
984
- window.location.assign(this.options.root + fragment);
985
- }
986
- if (options.trigger) this.loadUrl(fragment);
987
- },
988
-
989
- // Update the hash location, either replacing the current entry, or adding
990
- // a new one to the browser history.
991
- _updateHash: function(location, fragment, replace) {
992
- if (replace) {
993
- location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
994
- } else {
995
- location.hash = fragment;
996
- }
997
- }
998
- });
999
-
1000
- // Backbone.View
1001
- // -------------
1002
-
1003
- // Creating a Backbone.View creates its initial element outside of the DOM,
1004
- // if an existing element is not provided...
1005
- Backbone.View = function(options) {
1006
- this.cid = _.uniqueId('view');
1007
- this._configure(options || {});
1008
- this._ensureElement();
1009
- this.initialize.apply(this, arguments);
1010
- this.delegateEvents();
1011
- };
1012
-
1013
- // Cached regex to split keys for `delegate`.
1014
- var eventSplitter = /^(\S+)\s*(.*)$/;
1015
-
1016
- // List of view options to be merged as properties.
1017
- var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
1018
-
1019
- // Set up all inheritable **Backbone.View** properties and methods.
1020
- _.extend(Backbone.View.prototype, Backbone.Events, {
1021
-
1022
- // The default `tagName` of a View's element is `"div"`.
1023
- tagName: 'div',
1024
-
1025
- // jQuery delegate for element lookup, scoped to DOM elements within the
1026
- // current view. This should be prefered to global lookups where possible.
1027
- $: function(selector) {
1028
- return this.$el.find(selector);
1029
- },
1030
-
1031
- // Initialize is an empty function by default. Override it with your own
1032
- // initialization logic.
1033
- initialize: function(){},
1034
-
1035
- // **render** is the core function that your view should override, in order
1036
- // to populate its element (`this.el`), with the appropriate HTML. The
1037
- // convention is for **render** to always return `this`.
1038
- render: function() {
1039
- return this;
1040
- },
1041
-
1042
- // Remove this view from the DOM. Note that the view isn't present in the
1043
- // DOM by default, so calling this method may be a no-op.
1044
- remove: function() {
1045
- this.$el.remove();
1046
- return this;
1047
- },
1048
-
1049
- // For small amounts of DOM Elements, where a full-blown template isn't
1050
- // needed, use **make** to manufacture elements, one at a time.
1051
- //
1052
- // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
1053
- //
1054
- make: function(tagName, attributes, content) {
1055
- var el = document.createElement(tagName);
1056
- if (attributes) $(el).attr(attributes);
1057
- if (content) $(el).html(content);
1058
- return el;
1059
- },
1060
-
1061
- // Change the view's element (`this.el` property), including event
1062
- // re-delegation.
1063
- setElement: function(element, delegate) {
1064
- this.$el = $(element);
1065
- this.el = this.$el[0];
1066
- if (delegate !== false) this.delegateEvents();
1067
- return this;
1068
- },
1069
-
1070
- // Set callbacks, where `this.events` is a hash of
1071
- //
1072
- // *{"event selector": "callback"}*
1073
- //
1074
- // {
1075
- // 'mousedown .title': 'edit',
1076
- // 'click .button': 'save'
1077
- // 'click .open': function(e) { ... }
1078
- // }
1079
- //
1080
- // pairs. Callbacks will be bound to the view, with `this` set properly.
1081
- // Uses event delegation for efficiency.
1082
- // Omitting the selector binds the event to `this.el`.
1083
- // This only works for delegate-able events: not `focus`, `blur`, and
1084
- // not `change`, `submit`, and `reset` in Internet Explorer.
1085
- delegateEvents: function(events) {
1086
- if (!(events || (events = getValue(this, 'events')))) return;
1087
- this.undelegateEvents();
1088
- for (var key in events) {
1089
- var method = events[key];
1090
- if (!_.isFunction(method)) method = this[events[key]];
1091
- if (!method) throw new Error('Event "' + events[key] + '" does not exist');
1092
- var match = key.match(eventSplitter);
1093
- var eventName = match[1], selector = match[2];
1094
- method = _.bind(method, this);
1095
- eventName += '.delegateEvents' + this.cid;
1096
- if (selector === '') {
1097
- this.$el.bind(eventName, method);
1098
- } else {
1099
- this.$el.delegate(selector, eventName, method);
1100
- }
1101
- }
1102
- },
1103
-
1104
- // Clears all callbacks previously bound to the view with `delegateEvents`.
1105
- // You usually don't need to use this, but may wish to if you have multiple
1106
- // Backbone views attached to the same DOM element.
1107
- undelegateEvents: function() {
1108
- this.$el.unbind('.delegateEvents' + this.cid);
1109
- },
1110
-
1111
- // Performs the initial configuration of a View with a set of options.
1112
- // Keys with special meaning *(model, collection, id, className)*, are
1113
- // attached directly to the view.
1114
- _configure: function(options) {
1115
- if (this.options) options = _.extend({}, this.options, options);
1116
- for (var i = 0, l = viewOptions.length; i < l; i++) {
1117
- var attr = viewOptions[i];
1118
- if (options[attr]) this[attr] = options[attr];
1119
- }
1120
- this.options = options;
1121
- },
1122
-
1123
- // Ensure that the View has a DOM element to render into.
1124
- // If `this.el` is a string, pass it through `$()`, take the first
1125
- // matching element, and re-assign it to `el`. Otherwise, create
1126
- // an element from the `id`, `className` and `tagName` properties.
1127
- _ensureElement: function() {
1128
- if (!this.el) {
1129
- var attrs = getValue(this, 'attributes') || {};
1130
- if (this.id) attrs.id = this.id;
1131
- if (this.className) attrs['class'] = this.className;
1132
- this.setElement(this.make(this.tagName, attrs), false);
1133
- } else {
1134
- this.setElement(this.el, false);
1135
- }
1136
- }
1137
-
1138
- });
1139
-
1140
- // The self-propagating extend function that Backbone classes use.
1141
- var extend = function (protoProps, classProps) {
1142
- var child = inherits(this, protoProps, classProps);
1143
- child.extend = this.extend;
1144
- return child;
1145
- };
1146
-
1147
- // Set up inheritance for the model, collection, and view.
1148
- Backbone.Model.extend = Backbone.Collection.extend =
1149
- Backbone.Router.extend = Backbone.View.extend = extend;
1150
-
1151
- // Backbone.sync
1152
- // -------------
1153
-
1154
- // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1155
- var methodMap = {
1156
- 'create': 'POST',
1157
- 'update': 'PUT',
1158
- 'delete': 'DELETE',
1159
- 'read': 'GET'
1160
- };
1161
-
1162
- // Override this function to change the manner in which Backbone persists
1163
- // models to the server. You will be passed the type of request, and the
1164
- // model in question. By default, makes a RESTful Ajax request
1165
- // to the model's `url()`. Some possible customizations could be:
1166
- //
1167
- // * Use `setTimeout` to batch rapid-fire updates into a single request.
1168
- // * Send up the models as XML instead of JSON.
1169
- // * Persist models via WebSockets instead of Ajax.
1170
- //
1171
- // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1172
- // as `POST`, with a `_method` parameter containing the true HTTP method,
1173
- // as well as all requests with the body as `application/x-www-form-urlencoded`
1174
- // instead of `application/json` with the model in a param named `model`.
1175
- // Useful when interfacing with server-side languages like **PHP** that make
1176
- // it difficult to read the body of `PUT` requests.
1177
- Backbone.sync = function(method, model, options) {
1178
- var type = methodMap[method];
1179
-
1180
- // Default JSON-request options.
1181
- var params = {type: type, dataType: 'json'};
1182
-
1183
- // Ensure that we have a URL.
1184
- if (!options.url) {
1185
- params.url = getValue(model, 'url') || urlError();
1186
- }
1187
-
1188
- // Ensure that we have the appropriate request data.
1189
- if (!options.data && model && (method == 'create' || method == 'update')) {
1190
- params.contentType = 'application/json';
1191
- params.data = JSON.stringify(model.toJSON());
1192
- }
1193
-
1194
- // For older servers, emulate JSON by encoding the request into an HTML-form.
1195
- if (Backbone.emulateJSON) {
1196
- params.contentType = 'application/x-www-form-urlencoded';
1197
- params.data = params.data ? {model: params.data} : {};
1198
- }
1199
-
1200
- // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1201
- // And an `X-HTTP-Method-Override` header.
1202
- if (Backbone.emulateHTTP) {
1203
- if (type === 'PUT' || type === 'DELETE') {
1204
- if (Backbone.emulateJSON) params.data._method = type;
1205
- params.type = 'POST';
1206
- params.beforeSend = function(xhr) {
1207
- xhr.setRequestHeader('X-HTTP-Method-Override', type);
1208
- };
1209
- }
1210
- }
1211
-
1212
- // Don't process data on a non-GET request.
1213
- if (params.type !== 'GET' && !Backbone.emulateJSON) {
1214
- params.processData = false;
1215
- }
1216
-
1217
- // Make the request, allowing the user to override any Ajax options.
1218
- return $.ajax(_.extend(params, options));
1219
- };
1220
-
1221
- // Wrap an optional error callback with a fallback error event.
1222
- Backbone.wrapError = function(onError, originalModel, options) {
1223
- return function(model, resp) {
1224
- resp = model === originalModel ? resp : model;
1225
- if (onError) {
1226
- onError(originalModel, resp, options);
1227
- } else {
1228
- originalModel.trigger('error', originalModel, resp, options);
1229
- }
1230
- };
1231
- };
1232
-
1233
- // Helpers
1234
- // -------
1235
-
1236
- // Shared empty constructor function to aid in prototype-chain creation.
1237
- var ctor = function(){};
1238
-
1239
- // Helper function to correctly set up the prototype chain, for subclasses.
1240
- // Similar to `goog.inherits`, but uses a hash of prototype properties and
1241
- // class properties to be extended.
1242
- var inherits = function(parent, protoProps, staticProps) {
1243
- var child;
1244
-
1245
- // The constructor function for the new subclass is either defined by you
1246
- // (the "constructor" property in your `extend` definition), or defaulted
1247
- // by us to simply call the parent's constructor.
1248
- if (protoProps && protoProps.hasOwnProperty('constructor')) {
1249
- child = protoProps.constructor;
1250
- } else {
1251
- child = function(){ parent.apply(this, arguments); };
1252
- }
1253
-
1254
- // Inherit class (static) properties from parent.
1255
- _.extend(child, parent);
1256
-
1257
- // Set the prototype chain to inherit from `parent`, without calling
1258
- // `parent`'s constructor function.
1259
- ctor.prototype = parent.prototype;
1260
- child.prototype = new ctor();
1261
-
1262
- // Add prototype properties (instance properties) to the subclass,
1263
- // if supplied.
1264
- if (protoProps) _.extend(child.prototype, protoProps);
1265
-
1266
- // Add static properties to the constructor function, if supplied.
1267
- if (staticProps) _.extend(child, staticProps);
1268
-
1269
- // Correctly set child's `prototype.constructor`.
1270
- child.prototype.constructor = child;
1271
-
1272
- // Set a convenience property in case the parent's prototype is needed later.
1273
- child.__super__ = parent.prototype;
1274
-
1275
- return child;
1276
- };
1277
-
1278
- // Helper function to get a value from a Backbone object as a property
1279
- // or as a function.
1280
- var getValue = function(object, prop) {
1281
- if (!(object && object[prop])) return null;
1282
- return _.isFunction(object[prop]) ? object[prop]() : object[prop];
1283
- };
1284
-
1285
- // Throw an error when a URL is needed, and none is supplied.
1286
- var urlError = function() {
1287
- throw new Error('A "url" property or function must be specified');
1288
- };
1289
-
1290
- }).call(this);