├── .gitignore
├── MIT-LICENSE
├── README.md
├── karma.conf.js.FIXME
├── src
└── view-model.js
└── test
├── basic.html
├── lib
├── backbone-0.9.2.js
├── qunit-1.10.0.css
├── qunit-1.10.0.js
├── require.js
└── underscore-1.3.1.js
├── require-js.html
├── require-js.test.js
└── view-model.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | node_modules
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Tom Hallett
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Why do I need a Backbone.ViewModel?
2 |
3 | `Backbone.Model`'s are great at storing the state of your objects and persisting them back to your server. But as your `Backbone.View`'s become more complex, it's useful to have a model to store all view related attributes. These view attributes should be stored on a separate model than persistence attributes for 2 reasons:
4 |
5 | * Separation of concerns
6 | * If you store view attributes on the same model as your persistence model, when you call `.save()` on that model, the view attributes will be sent to the server too, eek!
7 |
8 | ## Usage
9 |
10 | Let's say you have a fairly standard Model and View:
11 |
12 | ```javascript
13 | // defining your classes
14 | var Tweet = Backbone.Model.extend({});
15 | var TweetView = Backbone.View.extend({});
16 |
17 | // creating an instance of your model
18 | var myTweet = new Tweet({text: "I love backbone!"});
19 | ```
20 |
21 | You define a `Backbone.ViewModel` class with a `computed_attributes` object:
22 |
23 | ```javascript
24 | var TweetViewModel = Backbone.ViewModel.extend({
25 | computed_attributes: {
26 | "truncated_text" : function(){
27 | return this.get("source_model").get("text").substring(0,10) + "…";
28 | },
29 | "escaped_text" : function(){
30 | return encodeURIComponent(this.get("source_model").get("text"));
31 | }
32 | }
33 | });
34 | ```
35 |
36 | Initialize your `ViewModel` and pass your persistence model as `source_model`:
37 |
38 | ```javascript
39 | var myTweetViewModel = new TweetViewModel({
40 | source_model: myTweet
41 | });
42 | ```
43 |
44 |
45 | Now, pass your `ViewModel` to your `View`:
46 |
47 | ```javascript
48 | var myTweetView = new TweetView({
49 | model: myTweetViewModel
50 | });
51 | ```
52 |
53 |
54 | When your `ViewModel` is initialized or whenever any attribute on your `source_model` changes, all of the `computed_attributes` will be processed and `set` on your `ViewModel`:
55 |
56 |
57 | ```javascript
58 | // The view attributes are set on the ViewModel
59 | myTweetViewModel.get("truncated_text") // => "I love bac…"
60 |
61 | // They can be used easily in your View Template
62 | {{ truncated_text }} View more
63 | ```
64 |
65 | If your `computed_attributes` depend on multiple source models, you can initialize your `ViewModel` with a `source_models` attribute that contains a mapping of attribute to model pairs:
66 |
67 | ```javascript
68 | var myOldestTweet = new Tweet({text: "Just joined twitter!"});
69 | var myNewestTweet = new Tweet({text: "I love backbone!"});
70 |
71 | var TweetSummaryViewModel = Backbone.ViewModel.extend({
72 | computed_attributes: {
73 | alpha_omega: function(){
74 | return this.get("source_models").oldestTweet.get("text") + "…" + this.get("source_models").newestTweet.get("text");
75 | }
76 | }
77 | });
78 |
79 | var myTweetSummary = new TweetSummaryViewModel({
80 | source_models: {
81 | oldestTweet: myOldestTweet,
82 | newestTweet: myNewestTweet
83 | }
84 | });
85 | ```
86 |
87 | As with the single `source_model` approach the `computed_attributes` will be processed when the `ViewModel` is created, and when any of the `source_models` change.
88 |
89 | ```javascript
90 | console.log(myTweetSummary.get("alpha_omega")); // Prints 'Just joined twitter!…I love backbone!'.
91 |
92 | myNewestTweet.set({text: "Taking a lunch break"});
93 |
94 | console.log(myTweetSummary.get("alpha_omega")); // Prints 'Just joined twitter!…Taking a lunch break'.
95 | ```
96 |
97 | ## Installation
98 |
99 | To install, include the `src/view-model.js` file in your HTML page, after Backbone and it's dependencies.
100 |
101 |
102 | ## Testing
103 |
104 | This project uses QUnit for it's automated tests.
105 |
106 | You can run the automated tests in one of two ways:
107 |
108 | 1. Open the following files in your browser: `backbone-view-model/test/basic.html` and `backbone-view-model/test/require-js.html`.
109 |
110 | 2. Karma: Right now this is broken. TODO - can someone help fix this?
111 |
112 | ## Contributing
113 |
114 | * Make sure the tests are green.
115 | * Add tests for your new features/fixes, so I don't break them in the future.
116 | * Add documentation to the README, so people know about your new feature.
117 |
--------------------------------------------------------------------------------
/karma.conf.js.FIXME:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | module.exports = function(config) {
3 | config.set({
4 | // base path, that will be used to resolve files and exclude
5 | basePath: '',
6 |
7 | frameworks: ["qunit"],
8 |
9 | // list of files / patterns to load in the browser
10 | files: [
11 | 'test/lib/underscore-1.3.1.js',
12 | 'test/lib/backbone-0.9.2.js',
13 | 'test/lib/require.js',
14 | 'src/*.js',
15 | 'test/*.js'
16 | ],
17 |
18 | // list of files to exclude
19 | exclude: [],
20 |
21 | // test results reporter to use
22 | // possible values: 'dots', 'progress', 'junit'
23 | reporters: ['progress'],
24 |
25 |
26 | // web server port
27 | port: 9876,
28 |
29 |
30 | // cli runner port
31 | runnerPort: 9100,
32 |
33 |
34 | // enable / disable colors in the output (reporters and logs)
35 | colors: true,
36 |
37 |
38 | // level of logging
39 | logLevel: config.LOG_INFO,
40 |
41 |
42 | // enable / disable watching file and executing tests whenever any file changes
43 | autoWatch: true,
44 |
45 |
46 | // Start these browsers, currently available:
47 | // - Chrome
48 | // - ChromeCanary
49 | // - Firefox
50 | // - Opera
51 | // - Safari (only Mac)
52 | // - PhantomJS
53 | // - IE (only Windows)
54 | browsers: ['Chrome'],
55 |
56 |
57 | // If browser does not capture in given timeout [ms], kill it
58 | captureTimeout: 60000,
59 |
60 |
61 | // Continuous Integration mode
62 | // if true, it capture browsers, run tests and exit
63 | singleRun: false
64 | });
65 | };
66 |
--------------------------------------------------------------------------------
/src/view-model.js:
--------------------------------------------------------------------------------
1 | // Backbone.ViewModel v0.1.0
2 | //
3 | // Copyright (C)2012 Tom Hallett
4 | // Distributed Under MIT License
5 | //
6 | // Documentation and Full License Available at:
7 | // http://github.com/tommyh/backbone-view-model
8 |
9 | (function (root, factory) {
10 | if (typeof define === 'function' && define.amd) {
11 | define(["underscore", "backbone"], factory);
12 | } else {
13 | factory();
14 | }
15 | }(this, function() {
16 |
17 | 'use strict';
18 |
19 | Backbone.ViewModel = (function () {
20 | var Model = Backbone.Model,
21 | ViewModel = function(attributes, options) {
22 | Model.apply(this, [attributes, options]);
23 | this.initializeViewModel();
24 | };
25 |
26 | _.extend(ViewModel.prototype, Model.prototype, {
27 |
28 | initializeViewModel: function(){
29 | this.setComputedAttributes();
30 | this.bindToChangesInSourceModel();
31 | },
32 |
33 | setComputedAttributes: function(){
34 | _.each(this.computed_attributes, function(value, key){
35 | this.set(key, value.call(this));
36 | }, this);
37 | },
38 |
39 | bindToChangesInSourceModel: function(){
40 | var sourceModel = this.get('source_model') || [],
41 | sourceModels = _.values(this.get('source_models'));
42 |
43 | _.each(_.union(sourceModel, sourceModels), function(model){
44 | model.on("change", this.setComputedAttributes, this);
45 | }, this);
46 | }
47 |
48 | });
49 |
50 | ViewModel.extend = Model.extend;
51 |
52 | return ViewModel;
53 | })();
54 | }));
55 |
--------------------------------------------------------------------------------
/test/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | backbone-view-model tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/lib/backbone-0.9.2.js:
--------------------------------------------------------------------------------
1 | // Backbone.js 0.9.2
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.2';
36 |
37 | // Require Underscore, if we're on the server, and it's not already present.
38 | var _ = root._;
39 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
40 |
41 | // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
42 | var $ = root.jQuery || root.Zepto || root.ender;
43 |
44 | // Set the JavaScript library that will be used for DOM manipulation and
45 | // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
46 | // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
47 | // alternate JavaScript library (or a mock library for testing your views
48 | // outside of a browser).
49 | Backbone.setDomLibrary = function(lib) {
50 | $ = lib;
51 | };
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 | // Regular expression used to split event strings
75 | var eventSplitter = /\s+/;
76 |
77 | // A module that can be mixed in to *any object* in order to provide it with
78 | // custom events. You may bind with `on` or remove with `off` callback functions
79 | // to an event; trigger`-ing an event fires all callbacks in succession.
80 | //
81 | // var object = {};
82 | // _.extend(object, Backbone.Events);
83 | // object.on('expand', function(){ alert('expanded'); });
84 | // object.trigger('expand');
85 | //
86 | var Events = Backbone.Events = {
87 |
88 | // Bind one or more space separated events, `events`, to a `callback`
89 | // function. Passing `"all"` will bind the callback to all events fired.
90 | on: function(events, callback, context) {
91 |
92 | var calls, event, node, tail, list;
93 | if (!callback) return this;
94 | events = events.split(eventSplitter);
95 | calls = this._callbacks || (this._callbacks = {});
96 |
97 | // Create an immutable callback list, allowing traversal during
98 | // modification. The tail is an empty object that will always be used
99 | // as the next node.
100 | while (event = events.shift()) {
101 | list = calls[event];
102 | node = list ? list.tail : {};
103 | node.next = tail = {};
104 | node.context = context;
105 | node.callback = callback;
106 | calls[event] = {tail: tail, next: list ? list.next : node};
107 | }
108 |
109 | return this;
110 | },
111 |
112 | // Remove one or many callbacks. If `context` is null, removes all callbacks
113 | // with that function. If `callback` is null, removes all callbacks for the
114 | // event. If `events` is null, removes all bound callbacks for all events.
115 | off: function(events, callback, context) {
116 | var event, calls, node, tail, cb, ctx;
117 |
118 | // No events, or removing *all* events.
119 | if (!(calls = this._callbacks)) return;
120 | if (!(events || callback || context)) {
121 | delete this._callbacks;
122 | return this;
123 | }
124 |
125 | // Loop through the listed events and contexts, splicing them out of the
126 | // linked list of callbacks if appropriate.
127 | events = events ? events.split(eventSplitter) : _.keys(calls);
128 | while (event = events.shift()) {
129 | node = calls[event];
130 | delete calls[event];
131 | if (!node || !(callback || context)) continue;
132 | // Create a new list, omitting the indicated callbacks.
133 | tail = node.tail;
134 | while ((node = node.next) !== tail) {
135 | cb = node.callback;
136 | ctx = node.context;
137 | if ((callback && cb !== callback) || (context && ctx !== context)) {
138 | this.on(event, cb, ctx);
139 | }
140 | }
141 | }
142 |
143 | return this;
144 | },
145 |
146 | // Trigger one or many events, firing all bound callbacks. Callbacks are
147 | // passed the same arguments as `trigger` is, apart from the event name
148 | // (unless you're listening on `"all"`, which will cause your callback to
149 | // receive the true name of the event as the first argument).
150 | trigger: function(events) {
151 | var event, node, calls, tail, args, all, rest;
152 | if (!(calls = this._callbacks)) return this;
153 | all = calls.all;
154 | events = events.split(eventSplitter);
155 | rest = slice.call(arguments, 1);
156 |
157 | // For each event, walk through the linked list of callbacks twice,
158 | // first to trigger the event, then to trigger any `"all"` callbacks.
159 | while (event = events.shift()) {
160 | if (node = calls[event]) {
161 | tail = node.tail;
162 | while ((node = node.next) !== tail) {
163 | node.callback.apply(node.context || this, rest);
164 | }
165 | }
166 | if (node = all) {
167 | tail = node.tail;
168 | args = [event].concat(rest);
169 | while ((node = node.next) !== tail) {
170 | node.callback.apply(node.context || this, args);
171 | }
172 | }
173 | }
174 |
175 | return this;
176 | }
177 |
178 | };
179 |
180 | // Aliases for backwards compatibility.
181 | Events.bind = Events.on;
182 | Events.unbind = Events.off;
183 |
184 | // Backbone.Model
185 | // --------------
186 |
187 | // Create a new model, with defined attributes. A client id (`cid`)
188 | // is automatically generated and assigned for you.
189 | var Model = Backbone.Model = function(attributes, options) {
190 | var defaults;
191 | attributes || (attributes = {});
192 | if (options && options.parse) attributes = this.parse(attributes);
193 | if (defaults = getValue(this, 'defaults')) {
194 | attributes = _.extend({}, defaults, attributes);
195 | }
196 | if (options && options.collection) this.collection = options.collection;
197 | this.attributes = {};
198 | this._escapedAttributes = {};
199 | this.cid = _.uniqueId('c');
200 | this.changed = {};
201 | this._silent = {};
202 | this._pending = {};
203 | this.set(attributes, {silent: true});
204 | // Reset change tracking.
205 | this.changed = {};
206 | this._silent = {};
207 | this._pending = {};
208 | this._previousAttributes = _.clone(this.attributes);
209 | this.initialize.apply(this, arguments);
210 | };
211 |
212 | // Attach all inheritable methods to the Model prototype.
213 | _.extend(Model.prototype, Events, {
214 |
215 | // A hash of attributes whose current and previous value differ.
216 | changed: null,
217 |
218 | // A hash of attributes that have silently changed since the last time
219 | // `change` was called. Will become pending attributes on the next call.
220 | _silent: null,
221 |
222 | // A hash of attributes that have changed since the last `'change'` event
223 | // began.
224 | _pending: null,
225 |
226 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and
227 | // CouchDB users may want to set this to `"_id"`.
228 | idAttribute: 'id',
229 |
230 | // Initialize is an empty function by default. Override it with your own
231 | // initialization logic.
232 | initialize: function(){},
233 |
234 | // Return a copy of the model's `attributes` object.
235 | toJSON: function(options) {
236 | return _.clone(this.attributes);
237 | },
238 |
239 | // Get the value of an attribute.
240 | get: function(attr) {
241 | return this.attributes[attr];
242 | },
243 |
244 | // Get the HTML-escaped value of an attribute.
245 | escape: function(attr) {
246 | var html;
247 | if (html = this._escapedAttributes[attr]) return html;
248 | var val = this.get(attr);
249 | return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
250 | },
251 |
252 | // Returns `true` if the attribute contains a value that is not null
253 | // or undefined.
254 | has: function(attr) {
255 | return this.get(attr) != null;
256 | },
257 |
258 | // Set a hash of model attributes on the object, firing `"change"` unless
259 | // you choose to silence it.
260 | set: function(key, value, options) {
261 | var attrs, attr, val;
262 |
263 | // Handle both `"key", value` and `{key: value}` -style arguments.
264 | if (_.isObject(key) || key == null) {
265 | attrs = key;
266 | options = value;
267 | } else {
268 | attrs = {};
269 | attrs[key] = value;
270 | }
271 |
272 | // Extract attributes and options.
273 | options || (options = {});
274 | if (!attrs) return this;
275 | if (attrs instanceof Model) attrs = attrs.attributes;
276 | if (options.unset) for (attr in attrs) attrs[attr] = void 0;
277 |
278 | // Run validation.
279 | if (!this._validate(attrs, options)) return false;
280 |
281 | // Check for changes of `id`.
282 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
283 |
284 | var changes = options.changes = {};
285 | var now = this.attributes;
286 | var escaped = this._escapedAttributes;
287 | var prev = this._previousAttributes || {};
288 |
289 | // For each `set` attribute...
290 | for (attr in attrs) {
291 | val = attrs[attr];
292 |
293 | // If the new and current value differ, record the change.
294 | if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
295 | delete escaped[attr];
296 | (options.silent ? this._silent : changes)[attr] = true;
297 | }
298 |
299 | // Update or delete the current value.
300 | options.unset ? delete now[attr] : now[attr] = val;
301 |
302 | // If the new and previous value differ, record the change. If not,
303 | // then remove changes for this attribute.
304 | if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
305 | this.changed[attr] = val;
306 | if (!options.silent) this._pending[attr] = true;
307 | } else {
308 | delete this.changed[attr];
309 | delete this._pending[attr];
310 | }
311 | }
312 |
313 | // Fire the `"change"` events.
314 | if (!options.silent) this.change(options);
315 | return this;
316 | },
317 |
318 | // Remove an attribute from the model, firing `"change"` unless you choose
319 | // to silence it. `unset` is a noop if the attribute doesn't exist.
320 | unset: function(attr, options) {
321 | (options || (options = {})).unset = true;
322 | return this.set(attr, null, options);
323 | },
324 |
325 | // Clear all attributes on the model, firing `"change"` unless you choose
326 | // to silence it.
327 | clear: function(options) {
328 | (options || (options = {})).unset = true;
329 | return this.set(_.clone(this.attributes), options);
330 | },
331 |
332 | // Fetch the model from the server. If the server's representation of the
333 | // model differs from its current attributes, they will be overriden,
334 | // triggering a `"change"` event.
335 | fetch: function(options) {
336 | options = options ? _.clone(options) : {};
337 | var model = this;
338 | var success = options.success;
339 | options.success = function(resp, status, xhr) {
340 | if (!model.set(model.parse(resp, xhr), options)) return false;
341 | if (success) success(model, resp);
342 | };
343 | options.error = Backbone.wrapError(options.error, model, options);
344 | return (this.sync || Backbone.sync).call(this, 'read', this, options);
345 | },
346 |
347 | // Set a hash of model attributes, and sync the model to the server.
348 | // If the server returns an attributes hash that differs, the model's
349 | // state will be `set` again.
350 | save: function(key, value, options) {
351 | var attrs, current;
352 |
353 | // Handle both `("key", value)` and `({key: value})` -style calls.
354 | if (_.isObject(key) || key == null) {
355 | attrs = key;
356 | options = value;
357 | } else {
358 | attrs = {};
359 | attrs[key] = value;
360 | }
361 | options = options ? _.clone(options) : {};
362 |
363 | // If we're "wait"-ing to set changed attributes, validate early.
364 | if (options.wait) {
365 | if (!this._validate(attrs, options)) return false;
366 | current = _.clone(this.attributes);
367 | }
368 |
369 | // Regular saves `set` attributes before persisting to the server.
370 | var silentOptions = _.extend({}, options, {silent: true});
371 | if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
372 | return false;
373 | }
374 |
375 | // After a successful server-side save, the client is (optionally)
376 | // updated with the server-side state.
377 | var model = this;
378 | var success = options.success;
379 | options.success = function(resp, status, xhr) {
380 | var serverAttrs = model.parse(resp, xhr);
381 | if (options.wait) {
382 | delete options.wait;
383 | serverAttrs = _.extend(attrs || {}, serverAttrs);
384 | }
385 | if (!model.set(serverAttrs, options)) return false;
386 | if (success) {
387 | success(model, resp);
388 | } else {
389 | model.trigger('sync', model, resp, options);
390 | }
391 | };
392 |
393 | // Finish configuring and sending the Ajax request.
394 | options.error = Backbone.wrapError(options.error, model, options);
395 | var method = this.isNew() ? 'create' : 'update';
396 | var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
397 | if (options.wait) this.set(current, silentOptions);
398 | return xhr;
399 | },
400 |
401 | // Destroy this model on the server if it was already persisted.
402 | // Optimistically removes the model from its collection, if it has one.
403 | // If `wait: true` is passed, waits for the server to respond before removal.
404 | destroy: function(options) {
405 | options = options ? _.clone(options) : {};
406 | var model = this;
407 | var success = options.success;
408 |
409 | var triggerDestroy = function() {
410 | model.trigger('destroy', model, model.collection, options);
411 | };
412 |
413 | if (this.isNew()) {
414 | triggerDestroy();
415 | return false;
416 | }
417 |
418 | options.success = function(resp) {
419 | if (options.wait) triggerDestroy();
420 | if (success) {
421 | success(model, resp);
422 | } else {
423 | model.trigger('sync', model, resp, options);
424 | }
425 | };
426 |
427 | options.error = Backbone.wrapError(options.error, model, options);
428 | var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
429 | if (!options.wait) triggerDestroy();
430 | return xhr;
431 | },
432 |
433 | // Default URL for the model's representation on the server -- if you're
434 | // using Backbone's restful methods, override this to change the endpoint
435 | // that will be called.
436 | url: function() {
437 | var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
438 | if (this.isNew()) return base;
439 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
440 | },
441 |
442 | // **parse** converts a response into the hash of attributes to be `set` on
443 | // the model. The default implementation is just to pass the response along.
444 | parse: function(resp, xhr) {
445 | return resp;
446 | },
447 |
448 | // Create a new model with identical attributes to this one.
449 | clone: function() {
450 | return new this.constructor(this.attributes);
451 | },
452 |
453 | // A model is new if it has never been saved to the server, and lacks an id.
454 | isNew: function() {
455 | return this.id == null;
456 | },
457 |
458 | // Call this method to manually fire a `"change"` event for this model and
459 | // a `"change:attribute"` event for each changed attribute.
460 | // Calling this will cause all objects observing the model to update.
461 | change: function(options) {
462 | options || (options = {});
463 | var changing = this._changing;
464 | this._changing = true;
465 |
466 | // Silent changes become pending changes.
467 | for (var attr in this._silent) this._pending[attr] = true;
468 |
469 | // Silent changes are triggered.
470 | var changes = _.extend({}, options.changes, this._silent);
471 | this._silent = {};
472 | for (var attr in changes) {
473 | this.trigger('change:' + attr, this, this.get(attr), options);
474 | }
475 | if (changing) return this;
476 |
477 | // Continue firing `"change"` events while there are pending changes.
478 | while (!_.isEmpty(this._pending)) {
479 | this._pending = {};
480 | this.trigger('change', this, options);
481 | // Pending and silent changes still remain.
482 | for (var attr in this.changed) {
483 | if (this._pending[attr] || this._silent[attr]) continue;
484 | delete this.changed[attr];
485 | }
486 | this._previousAttributes = _.clone(this.attributes);
487 | }
488 |
489 | this._changing = false;
490 | return this;
491 | },
492 |
493 | // Determine if the model has changed since the last `"change"` event.
494 | // If you specify an attribute name, determine if that attribute has changed.
495 | hasChanged: function(attr) {
496 | if (!arguments.length) return !_.isEmpty(this.changed);
497 | return _.has(this.changed, attr);
498 | },
499 |
500 | // Return an object containing all the attributes that have changed, or
501 | // false if there are no changed attributes. Useful for determining what
502 | // parts of a view need to be updated and/or what attributes need to be
503 | // persisted to the server. Unset attributes will be set to undefined.
504 | // You can also pass an attributes object to diff against the model,
505 | // determining if there *would be* a change.
506 | changedAttributes: function(diff) {
507 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
508 | var val, changed = false, old = this._previousAttributes;
509 | for (var attr in diff) {
510 | if (_.isEqual(old[attr], (val = diff[attr]))) continue;
511 | (changed || (changed = {}))[attr] = val;
512 | }
513 | return changed;
514 | },
515 |
516 | // Get the previous value of an attribute, recorded at the time the last
517 | // `"change"` event was fired.
518 | previous: function(attr) {
519 | if (!arguments.length || !this._previousAttributes) return null;
520 | return this._previousAttributes[attr];
521 | },
522 |
523 | // Get all of the attributes of the model at the time of the previous
524 | // `"change"` event.
525 | previousAttributes: function() {
526 | return _.clone(this._previousAttributes);
527 | },
528 |
529 | // Check if the model is currently in a valid state. It's only possible to
530 | // get into an *invalid* state if you're using silent changes.
531 | isValid: function() {
532 | return !this.validate(this.attributes);
533 | },
534 |
535 | // Run validation against the next complete set of model attributes,
536 | // returning `true` if all is well. If a specific `error` callback has
537 | // been passed, call that instead of firing the general `"error"` event.
538 | _validate: function(attrs, options) {
539 | if (options.silent || !this.validate) return true;
540 | attrs = _.extend({}, this.attributes, attrs);
541 | var error = this.validate(attrs, options);
542 | if (!error) return true;
543 | if (options && options.error) {
544 | options.error(this, error, options);
545 | } else {
546 | this.trigger('error', this, error, options);
547 | }
548 | return false;
549 | }
550 |
551 | });
552 |
553 | // Backbone.Collection
554 | // -------------------
555 |
556 | // Provides a standard collection class for our sets of models, ordered
557 | // or unordered. If a `comparator` is specified, the Collection will maintain
558 | // its models in sort order, as they're added and removed.
559 | var Collection = Backbone.Collection = function(models, options) {
560 | options || (options = {});
561 | if (options.model) this.model = options.model;
562 | if (options.comparator) this.comparator = options.comparator;
563 | this._reset();
564 | this.initialize.apply(this, arguments);
565 | if (models) this.reset(models, {silent: true, parse: options.parse});
566 | };
567 |
568 | // Define the Collection's inheritable methods.
569 | _.extend(Collection.prototype, Events, {
570 |
571 | // The default model for a collection is just a **Backbone.Model**.
572 | // This should be overridden in most cases.
573 | model: Model,
574 |
575 | // Initialize is an empty function by default. Override it with your own
576 | // initialization logic.
577 | initialize: function(){},
578 |
579 | // The JSON representation of a Collection is an array of the
580 | // models' attributes.
581 | toJSON: function(options) {
582 | return this.map(function(model){ return model.toJSON(options); });
583 | },
584 |
585 | // Add a model, or list of models to the set. Pass **silent** to avoid
586 | // firing the `add` event for every new model.
587 | add: function(models, options) {
588 | var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
589 | options || (options = {});
590 | models = _.isArray(models) ? models.slice() : [models];
591 |
592 | // Begin by turning bare objects into model references, and preventing
593 | // invalid models or duplicate models from being added.
594 | for (i = 0, length = models.length; i < length; i++) {
595 | if (!(model = models[i] = this._prepareModel(models[i], options))) {
596 | throw new Error("Can't add an invalid model to a collection");
597 | }
598 | cid = model.cid;
599 | id = model.id;
600 | if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
601 | dups.push(i);
602 | continue;
603 | }
604 | cids[cid] = ids[id] = model;
605 | }
606 |
607 | // Remove duplicates.
608 | i = dups.length;
609 | while (i--) {
610 | models.splice(dups[i], 1);
611 | }
612 |
613 | // Listen to added models' events, and index models for lookup by
614 | // `id` and by `cid`.
615 | for (i = 0, length = models.length; i < length; i++) {
616 | (model = models[i]).on('all', this._onModelEvent, this);
617 | this._byCid[model.cid] = model;
618 | if (model.id != null) this._byId[model.id] = model;
619 | }
620 |
621 | // Insert models into the collection, re-sorting if needed, and triggering
622 | // `add` events unless silenced.
623 | this.length += length;
624 | index = options.at != null ? options.at : this.models.length;
625 | splice.apply(this.models, [index, 0].concat(models));
626 | if (this.comparator) this.sort({silent: true});
627 | if (options.silent) return this;
628 | for (i = 0, length = this.models.length; i < length; i++) {
629 | if (!cids[(model = this.models[i]).cid]) continue;
630 | options.index = i;
631 | model.trigger('add', model, this, options);
632 | }
633 | return this;
634 | },
635 |
636 | // Remove a model, or a list of models from the set. Pass silent to avoid
637 | // firing the `remove` event for every model removed.
638 | remove: function(models, options) {
639 | var i, l, index, model;
640 | options || (options = {});
641 | models = _.isArray(models) ? models.slice() : [models];
642 | for (i = 0, l = models.length; i < l; i++) {
643 | model = this.getByCid(models[i]) || this.get(models[i]);
644 | if (!model) continue;
645 | delete this._byId[model.id];
646 | delete this._byCid[model.cid];
647 | index = this.indexOf(model);
648 | this.models.splice(index, 1);
649 | this.length--;
650 | if (!options.silent) {
651 | options.index = index;
652 | model.trigger('remove', model, this, options);
653 | }
654 | this._removeReference(model);
655 | }
656 | return this;
657 | },
658 |
659 | // Add a model to the end of the collection.
660 | push: function(model, options) {
661 | model = this._prepareModel(model, options);
662 | this.add(model, options);
663 | return model;
664 | },
665 |
666 | // Remove a model from the end of the collection.
667 | pop: function(options) {
668 | var model = this.at(this.length - 1);
669 | this.remove(model, options);
670 | return model;
671 | },
672 |
673 | // Add a model to the beginning of the collection.
674 | unshift: function(model, options) {
675 | model = this._prepareModel(model, options);
676 | this.add(model, _.extend({at: 0}, options));
677 | return model;
678 | },
679 |
680 | // Remove a model from the beginning of the collection.
681 | shift: function(options) {
682 | var model = this.at(0);
683 | this.remove(model, options);
684 | return model;
685 | },
686 |
687 | // Get a model from the set by id.
688 | get: function(id) {
689 | if (id == null) return void 0;
690 | return this._byId[id.id != null ? id.id : id];
691 | },
692 |
693 | // Get a model from the set by client id.
694 | getByCid: function(cid) {
695 | return cid && this._byCid[cid.cid || cid];
696 | },
697 |
698 | // Get the model at the given index.
699 | at: function(index) {
700 | return this.models[index];
701 | },
702 |
703 | // Return models with matching attributes. Useful for simple cases of `filter`.
704 | where: function(attrs) {
705 | if (_.isEmpty(attrs)) return [];
706 | return this.filter(function(model) {
707 | for (var key in attrs) {
708 | if (attrs[key] !== model.get(key)) return false;
709 | }
710 | return true;
711 | });
712 | },
713 |
714 | // Force the collection to re-sort itself. You don't need to call this under
715 | // normal circumstances, as the set will maintain sort order as each item
716 | // is added.
717 | sort: function(options) {
718 | options || (options = {});
719 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
720 | var boundComparator = _.bind(this.comparator, this);
721 | if (this.comparator.length == 1) {
722 | this.models = this.sortBy(boundComparator);
723 | } else {
724 | this.models.sort(boundComparator);
725 | }
726 | if (!options.silent) this.trigger('reset', this, options);
727 | return this;
728 | },
729 |
730 | // Pluck an attribute from each model in the collection.
731 | pluck: function(attr) {
732 | return _.map(this.models, function(model){ return model.get(attr); });
733 | },
734 |
735 | // When you have more items than you want to add or remove individually,
736 | // you can reset the entire set with a new list of models, without firing
737 | // any `add` or `remove` events. Fires `reset` when finished.
738 | reset: function(models, options) {
739 | models || (models = []);
740 | options || (options = {});
741 | for (var i = 0, l = this.models.length; i < l; i++) {
742 | this._removeReference(this.models[i]);
743 | }
744 | this._reset();
745 | this.add(models, _.extend({silent: true}, options));
746 | if (!options.silent) this.trigger('reset', this, options);
747 | return this;
748 | },
749 |
750 | // Fetch the default set of models for this collection, resetting the
751 | // collection when they arrive. If `add: true` is passed, appends the
752 | // models to the collection instead of resetting.
753 | fetch: function(options) {
754 | options = options ? _.clone(options) : {};
755 | if (options.parse === undefined) options.parse = true;
756 | var collection = this;
757 | var success = options.success;
758 | options.success = function(resp, status, xhr) {
759 | collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
760 | if (success) success(collection, resp);
761 | };
762 | options.error = Backbone.wrapError(options.error, collection, options);
763 | return (this.sync || Backbone.sync).call(this, 'read', this, options);
764 | },
765 |
766 | // Create a new instance of a model in this collection. Add the model to the
767 | // collection immediately, unless `wait: true` is passed, in which case we
768 | // wait for the server to agree.
769 | create: function(model, options) {
770 | var coll = this;
771 | options = options ? _.clone(options) : {};
772 | model = this._prepareModel(model, options);
773 | if (!model) return false;
774 | if (!options.wait) coll.add(model, options);
775 | var success = options.success;
776 | options.success = function(nextModel, resp, xhr) {
777 | if (options.wait) coll.add(nextModel, options);
778 | if (success) {
779 | success(nextModel, resp);
780 | } else {
781 | nextModel.trigger('sync', model, resp, options);
782 | }
783 | };
784 | model.save(null, options);
785 | return model;
786 | },
787 |
788 | // **parse** converts a response into a list of models to be added to the
789 | // collection. The default implementation is just to pass it through.
790 | parse: function(resp, xhr) {
791 | return resp;
792 | },
793 |
794 | // Proxy to _'s chain. Can't be proxied the same way the rest of the
795 | // underscore methods are proxied because it relies on the underscore
796 | // constructor.
797 | chain: function () {
798 | return _(this.models).chain();
799 | },
800 |
801 | // Reset all internal state. Called when the collection is reset.
802 | _reset: function(options) {
803 | this.length = 0;
804 | this.models = [];
805 | this._byId = {};
806 | this._byCid = {};
807 | },
808 |
809 | // Prepare a model or hash of attributes to be added to this collection.
810 | _prepareModel: function(model, options) {
811 | options || (options = {});
812 | if (!(model instanceof Model)) {
813 | var attrs = model;
814 | options.collection = this;
815 | model = new this.model(attrs, options);
816 | if (!model._validate(model.attributes, options)) model = false;
817 | } else if (!model.collection) {
818 | model.collection = this;
819 | }
820 | return model;
821 | },
822 |
823 | // Internal method to remove a model's ties to a collection.
824 | _removeReference: function(model) {
825 | if (this == model.collection) {
826 | delete model.collection;
827 | }
828 | model.off('all', this._onModelEvent, this);
829 | },
830 |
831 | // Internal method called every time a model in the set fires an event.
832 | // Sets need to update their indexes when models change ids. All other
833 | // events simply proxy through. "add" and "remove" events that originate
834 | // in other collections are ignored.
835 | _onModelEvent: function(event, model, collection, options) {
836 | if ((event == 'add' || event == 'remove') && collection != this) return;
837 | if (event == 'destroy') {
838 | this.remove(model, options);
839 | }
840 | if (model && event === 'change:' + model.idAttribute) {
841 | delete this._byId[model.previous(model.idAttribute)];
842 | this._byId[model.id] = model;
843 | }
844 | this.trigger.apply(this, arguments);
845 | }
846 |
847 | });
848 |
849 | // Underscore methods that we want to implement on the Collection.
850 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
851 | 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
852 | 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
853 | 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
854 | 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
855 |
856 | // Mix in each Underscore method as a proxy to `Collection#models`.
857 | _.each(methods, function(method) {
858 | Collection.prototype[method] = function() {
859 | return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
860 | };
861 | });
862 |
863 | // Backbone.Router
864 | // -------------------
865 |
866 | // Routers map faux-URLs to actions, and fire events when routes are
867 | // matched. Creating a new one sets its `routes` hash, if not set statically.
868 | var Router = Backbone.Router = function(options) {
869 | options || (options = {});
870 | if (options.routes) this.routes = options.routes;
871 | this._bindRoutes();
872 | this.initialize.apply(this, arguments);
873 | };
874 |
875 | // Cached regular expressions for matching named param parts and splatted
876 | // parts of route strings.
877 | var namedParam = /:\w+/g;
878 | var splatParam = /\*\w+/g;
879 | var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
880 |
881 | // Set up all inheritable **Backbone.Router** properties and methods.
882 | _.extend(Router.prototype, Events, {
883 |
884 | // Initialize is an empty function by default. Override it with your own
885 | // initialization logic.
886 | initialize: function(){},
887 |
888 | // Manually bind a single named route to a callback. For example:
889 | //
890 | // this.route('search/:query/p:num', 'search', function(query, num) {
891 | // ...
892 | // });
893 | //
894 | route: function(route, name, callback) {
895 | Backbone.history || (Backbone.history = new History);
896 | if (!_.isRegExp(route)) route = this._routeToRegExp(route);
897 | if (!callback) callback = this[name];
898 | Backbone.history.route(route, _.bind(function(fragment) {
899 | var args = this._extractParameters(route, fragment);
900 | callback && callback.apply(this, args);
901 | this.trigger.apply(this, ['route:' + name].concat(args));
902 | Backbone.history.trigger('route', this, name, args);
903 | }, this));
904 | return this;
905 | },
906 |
907 | // Simple proxy to `Backbone.history` to save a fragment into the history.
908 | navigate: function(fragment, options) {
909 | Backbone.history.navigate(fragment, options);
910 | },
911 |
912 | // Bind all defined routes to `Backbone.history`. We have to reverse the
913 | // order of the routes here to support behavior where the most general
914 | // routes can be defined at the bottom of the route map.
915 | _bindRoutes: function() {
916 | if (!this.routes) return;
917 | var routes = [];
918 | for (var route in this.routes) {
919 | routes.unshift([route, this.routes[route]]);
920 | }
921 | for (var i = 0, l = routes.length; i < l; i++) {
922 | this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
923 | }
924 | },
925 |
926 | // Convert a route string into a regular expression, suitable for matching
927 | // against the current location hash.
928 | _routeToRegExp: function(route) {
929 | route = route.replace(escapeRegExp, '\\$&')
930 | .replace(namedParam, '([^\/]+)')
931 | .replace(splatParam, '(.*?)');
932 | return new RegExp('^' + route + '$');
933 | },
934 |
935 | // Given a route, and a URL fragment that it matches, return the array of
936 | // extracted parameters.
937 | _extractParameters: function(route, fragment) {
938 | return route.exec(fragment).slice(1);
939 | }
940 |
941 | });
942 |
943 | // Backbone.History
944 | // ----------------
945 |
946 | // Handles cross-browser history management, based on URL fragments. If the
947 | // browser does not support `onhashchange`, falls back to polling.
948 | var History = Backbone.History = function() {
949 | this.handlers = [];
950 | _.bindAll(this, 'checkUrl');
951 | };
952 |
953 | // Cached regex for cleaning leading hashes and slashes .
954 | var routeStripper = /^[#\/]/;
955 |
956 | // Cached regex for detecting MSIE.
957 | var isExplorer = /msie [\w.]+/;
958 |
959 | // Has the history handling already been started?
960 | History.started = false;
961 |
962 | // Set up all inheritable **Backbone.History** properties and methods.
963 | _.extend(History.prototype, Events, {
964 |
965 | // The default interval to poll for hash changes, if necessary, is
966 | // twenty times a second.
967 | interval: 50,
968 |
969 | // Gets the true hash value. Cannot use location.hash directly due to bug
970 | // in Firefox where location.hash will always be decoded.
971 | getHash: function(windowOverride) {
972 | var loc = windowOverride ? windowOverride.location : window.location;
973 | var match = loc.href.match(/#(.*)$/);
974 | return match ? match[1] : '';
975 | },
976 |
977 | // Get the cross-browser normalized URL fragment, either from the URL,
978 | // the hash, or the override.
979 | getFragment: function(fragment, forcePushState) {
980 | if (fragment == null) {
981 | if (this._hasPushState || forcePushState) {
982 | fragment = window.location.pathname;
983 | var search = window.location.search;
984 | if (search) fragment += search;
985 | } else {
986 | fragment = this.getHash();
987 | }
988 | }
989 | if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
990 | return fragment.replace(routeStripper, '');
991 | },
992 |
993 | // Start the hash change handling, returning `true` if the current URL matches
994 | // an existing route, and `false` otherwise.
995 | start: function(options) {
996 | if (History.started) throw new Error("Backbone.history has already been started");
997 | History.started = true;
998 |
999 | // Figure out the initial configuration. Do we need an iframe?
1000 | // Is pushState desired ... is it available?
1001 | this.options = _.extend({}, {root: '/'}, this.options, options);
1002 | this._wantsHashChange = this.options.hashChange !== false;
1003 | this._wantsPushState = !!this.options.pushState;
1004 | this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
1005 | var fragment = this.getFragment();
1006 | var docMode = document.documentMode;
1007 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1008 |
1009 | if (oldIE) {
1010 | this.iframe = $('').hide().appendTo('body')[0].contentWindow;
1011 | this.navigate(fragment);
1012 | }
1013 |
1014 | // Depending on whether we're using pushState or hashes, and whether
1015 | // 'onhashchange' is supported, determine how we check the URL state.
1016 | if (this._hasPushState) {
1017 | $(window).bind('popstate', this.checkUrl);
1018 | } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1019 | $(window).bind('hashchange', this.checkUrl);
1020 | } else if (this._wantsHashChange) {
1021 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1022 | }
1023 |
1024 | // Determine if we need to change the base url, for a pushState link
1025 | // opened by a non-pushState browser.
1026 | this.fragment = fragment;
1027 | var loc = window.location;
1028 | var atRoot = loc.pathname == this.options.root;
1029 |
1030 | // If we've started off with a route from a `pushState`-enabled browser,
1031 | // but we're currently in a browser that doesn't support it...
1032 | if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1033 | this.fragment = this.getFragment(null, true);
1034 | window.location.replace(this.options.root + '#' + this.fragment);
1035 | // Return immediately as browser will do redirect to new url
1036 | return true;
1037 |
1038 | // Or if we've started out with a hash-based route, but we're currently
1039 | // in a browser where it could be `pushState`-based instead...
1040 | } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1041 | this.fragment = this.getHash().replace(routeStripper, '');
1042 | window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
1043 | }
1044 |
1045 | if (!this.options.silent) {
1046 | return this.loadUrl();
1047 | }
1048 | },
1049 |
1050 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1051 | // but possibly useful for unit testing Routers.
1052 | stop: function() {
1053 | $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
1054 | clearInterval(this._checkUrlInterval);
1055 | History.started = false;
1056 | },
1057 |
1058 | // Add a route to be tested when the fragment changes. Routes added later
1059 | // may override previous routes.
1060 | route: function(route, callback) {
1061 | this.handlers.unshift({route: route, callback: callback});
1062 | },
1063 |
1064 | // Checks the current URL to see if it has changed, and if it has,
1065 | // calls `loadUrl`, normalizing across the hidden iframe.
1066 | checkUrl: function(e) {
1067 | var current = this.getFragment();
1068 | if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
1069 | if (current == this.fragment) return false;
1070 | if (this.iframe) this.navigate(current);
1071 | this.loadUrl() || this.loadUrl(this.getHash());
1072 | },
1073 |
1074 | // Attempt to load the current URL fragment. If a route succeeds with a
1075 | // match, returns `true`. If no defined routes matches the fragment,
1076 | // returns `false`.
1077 | loadUrl: function(fragmentOverride) {
1078 | var fragment = this.fragment = this.getFragment(fragmentOverride);
1079 | var matched = _.any(this.handlers, function(handler) {
1080 | if (handler.route.test(fragment)) {
1081 | handler.callback(fragment);
1082 | return true;
1083 | }
1084 | });
1085 | return matched;
1086 | },
1087 |
1088 | // Save a fragment into the hash history, or replace the URL state if the
1089 | // 'replace' option is passed. You are responsible for properly URL-encoding
1090 | // the fragment in advance.
1091 | //
1092 | // The options object can contain `trigger: true` if you wish to have the
1093 | // route callback be fired (not usually desirable), or `replace: true`, if
1094 | // you wish to modify the current URL without adding an entry to the history.
1095 | navigate: function(fragment, options) {
1096 | if (!History.started) return false;
1097 | if (!options || options === true) options = {trigger: options};
1098 | var frag = (fragment || '').replace(routeStripper, '');
1099 | if (this.fragment == frag) return;
1100 |
1101 | // If pushState is available, we use it to set the fragment as a real URL.
1102 | if (this._hasPushState) {
1103 | if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
1104 | this.fragment = frag;
1105 | window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
1106 |
1107 | // If hash changes haven't been explicitly disabled, update the hash
1108 | // fragment to store history.
1109 | } else if (this._wantsHashChange) {
1110 | this.fragment = frag;
1111 | this._updateHash(window.location, frag, options.replace);
1112 | if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
1113 | // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
1114 | // When replace is true, we don't want this.
1115 | if(!options.replace) this.iframe.document.open().close();
1116 | this._updateHash(this.iframe.location, frag, options.replace);
1117 | }
1118 |
1119 | // If you've told us that you explicitly don't want fallback hashchange-
1120 | // based history, then `navigate` becomes a page refresh.
1121 | } else {
1122 | window.location.assign(this.options.root + fragment);
1123 | }
1124 | if (options.trigger) this.loadUrl(fragment);
1125 | },
1126 |
1127 | // Update the hash location, either replacing the current entry, or adding
1128 | // a new one to the browser history.
1129 | _updateHash: function(location, fragment, replace) {
1130 | if (replace) {
1131 | location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
1132 | } else {
1133 | location.hash = fragment;
1134 | }
1135 | }
1136 | });
1137 |
1138 | // Backbone.View
1139 | // -------------
1140 |
1141 | // Creating a Backbone.View creates its initial element outside of the DOM,
1142 | // if an existing element is not provided...
1143 | var View = Backbone.View = function(options) {
1144 | this.cid = _.uniqueId('view');
1145 | this._configure(options || {});
1146 | this._ensureElement();
1147 | this.initialize.apply(this, arguments);
1148 | this.delegateEvents();
1149 | };
1150 |
1151 | // Cached regex to split keys for `delegate`.
1152 | var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1153 |
1154 | // List of view options to be merged as properties.
1155 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
1156 |
1157 | // Set up all inheritable **Backbone.View** properties and methods.
1158 | _.extend(View.prototype, Events, {
1159 |
1160 | // The default `tagName` of a View's element is `"div"`.
1161 | tagName: 'div',
1162 |
1163 | // jQuery delegate for element lookup, scoped to DOM elements within the
1164 | // current view. This should be prefered to global lookups where possible.
1165 | $: function(selector) {
1166 | return this.$el.find(selector);
1167 | },
1168 |
1169 | // Initialize is an empty function by default. Override it with your own
1170 | // initialization logic.
1171 | initialize: function(){},
1172 |
1173 | // **render** is the core function that your view should override, in order
1174 | // to populate its element (`this.el`), with the appropriate HTML. The
1175 | // convention is for **render** to always return `this`.
1176 | render: function() {
1177 | return this;
1178 | },
1179 |
1180 | // Remove this view from the DOM. Note that the view isn't present in the
1181 | // DOM by default, so calling this method may be a no-op.
1182 | remove: function() {
1183 | this.$el.remove();
1184 | return this;
1185 | },
1186 |
1187 | // For small amounts of DOM Elements, where a full-blown template isn't
1188 | // needed, use **make** to manufacture elements, one at a time.
1189 | //
1190 | // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
1191 | //
1192 | make: function(tagName, attributes, content) {
1193 | var el = document.createElement(tagName);
1194 | if (attributes) $(el).attr(attributes);
1195 | if (content) $(el).html(content);
1196 | return el;
1197 | },
1198 |
1199 | // Change the view's element (`this.el` property), including event
1200 | // re-delegation.
1201 | setElement: function(element, delegate) {
1202 | if (this.$el) this.undelegateEvents();
1203 | this.$el = (element instanceof $) ? element : $(element);
1204 | this.el = this.$el[0];
1205 | if (delegate !== false) this.delegateEvents();
1206 | return this;
1207 | },
1208 |
1209 | // Set callbacks, where `this.events` is a hash of
1210 | //
1211 | // *{"event selector": "callback"}*
1212 | //
1213 | // {
1214 | // 'mousedown .title': 'edit',
1215 | // 'click .button': 'save'
1216 | // 'click .open': function(e) { ... }
1217 | // }
1218 | //
1219 | // pairs. Callbacks will be bound to the view, with `this` set properly.
1220 | // Uses event delegation for efficiency.
1221 | // Omitting the selector binds the event to `this.el`.
1222 | // This only works for delegate-able events: not `focus`, `blur`, and
1223 | // not `change`, `submit`, and `reset` in Internet Explorer.
1224 | delegateEvents: function(events) {
1225 | if (!(events || (events = getValue(this, 'events')))) return;
1226 | this.undelegateEvents();
1227 | for (var key in events) {
1228 | var method = events[key];
1229 | if (!_.isFunction(method)) method = this[events[key]];
1230 | if (!method) throw new Error('Method "' + events[key] + '" does not exist');
1231 | var match = key.match(delegateEventSplitter);
1232 | var eventName = match[1], selector = match[2];
1233 | method = _.bind(method, this);
1234 | eventName += '.delegateEvents' + this.cid;
1235 | if (selector === '') {
1236 | this.$el.bind(eventName, method);
1237 | } else {
1238 | this.$el.delegate(selector, eventName, method);
1239 | }
1240 | }
1241 | },
1242 |
1243 | // Clears all callbacks previously bound to the view with `delegateEvents`.
1244 | // You usually don't need to use this, but may wish to if you have multiple
1245 | // Backbone views attached to the same DOM element.
1246 | undelegateEvents: function() {
1247 | this.$el.unbind('.delegateEvents' + this.cid);
1248 | },
1249 |
1250 | // Performs the initial configuration of a View with a set of options.
1251 | // Keys with special meaning *(model, collection, id, className)*, are
1252 | // attached directly to the view.
1253 | _configure: function(options) {
1254 | if (this.options) options = _.extend({}, this.options, options);
1255 | for (var i = 0, l = viewOptions.length; i < l; i++) {
1256 | var attr = viewOptions[i];
1257 | if (options[attr]) this[attr] = options[attr];
1258 | }
1259 | this.options = options;
1260 | },
1261 |
1262 | // Ensure that the View has a DOM element to render into.
1263 | // If `this.el` is a string, pass it through `$()`, take the first
1264 | // matching element, and re-assign it to `el`. Otherwise, create
1265 | // an element from the `id`, `className` and `tagName` properties.
1266 | _ensureElement: function() {
1267 | if (!this.el) {
1268 | var attrs = getValue(this, 'attributes') || {};
1269 | if (this.id) attrs.id = this.id;
1270 | if (this.className) attrs['class'] = this.className;
1271 | this.setElement(this.make(this.tagName, attrs), false);
1272 | } else {
1273 | this.setElement(this.el, false);
1274 | }
1275 | }
1276 |
1277 | });
1278 |
1279 | // The self-propagating extend function that Backbone classes use.
1280 | var extend = function (protoProps, classProps) {
1281 | var child = inherits(this, protoProps, classProps);
1282 | child.extend = this.extend;
1283 | return child;
1284 | };
1285 |
1286 | // Set up inheritance for the model, collection, and view.
1287 | Model.extend = Collection.extend = Router.extend = View.extend = extend;
1288 |
1289 | // Backbone.sync
1290 | // -------------
1291 |
1292 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1293 | var methodMap = {
1294 | 'create': 'POST',
1295 | 'update': 'PUT',
1296 | 'delete': 'DELETE',
1297 | 'read': 'GET'
1298 | };
1299 |
1300 | // Override this function to change the manner in which Backbone persists
1301 | // models to the server. You will be passed the type of request, and the
1302 | // model in question. By default, makes a RESTful Ajax request
1303 | // to the model's `url()`. Some possible customizations could be:
1304 | //
1305 | // * Use `setTimeout` to batch rapid-fire updates into a single request.
1306 | // * Send up the models as XML instead of JSON.
1307 | // * Persist models via WebSockets instead of Ajax.
1308 | //
1309 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1310 | // as `POST`, with a `_method` parameter containing the true HTTP method,
1311 | // as well as all requests with the body as `application/x-www-form-urlencoded`
1312 | // instead of `application/json` with the model in a param named `model`.
1313 | // Useful when interfacing with server-side languages like **PHP** that make
1314 | // it difficult to read the body of `PUT` requests.
1315 | Backbone.sync = function(method, model, options) {
1316 | var type = methodMap[method];
1317 |
1318 | // Default options, unless specified.
1319 | options || (options = {});
1320 |
1321 | // Default JSON-request options.
1322 | var params = {type: type, dataType: 'json'};
1323 |
1324 | // Ensure that we have a URL.
1325 | if (!options.url) {
1326 | params.url = getValue(model, 'url') || urlError();
1327 | }
1328 |
1329 | // Ensure that we have the appropriate request data.
1330 | if (!options.data && model && (method == 'create' || method == 'update')) {
1331 | params.contentType = 'application/json';
1332 | params.data = JSON.stringify(model.toJSON());
1333 | }
1334 |
1335 | // For older servers, emulate JSON by encoding the request into an HTML-form.
1336 | if (Backbone.emulateJSON) {
1337 | params.contentType = 'application/x-www-form-urlencoded';
1338 | params.data = params.data ? {model: params.data} : {};
1339 | }
1340 |
1341 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1342 | // And an `X-HTTP-Method-Override` header.
1343 | if (Backbone.emulateHTTP) {
1344 | if (type === 'PUT' || type === 'DELETE') {
1345 | if (Backbone.emulateJSON) params.data._method = type;
1346 | params.type = 'POST';
1347 | params.beforeSend = function(xhr) {
1348 | xhr.setRequestHeader('X-HTTP-Method-Override', type);
1349 | };
1350 | }
1351 | }
1352 |
1353 | // Don't process data on a non-GET request.
1354 | if (params.type !== 'GET' && !Backbone.emulateJSON) {
1355 | params.processData = false;
1356 | }
1357 |
1358 | // Make the request, allowing the user to override any Ajax options.
1359 | return $.ajax(_.extend(params, options));
1360 | };
1361 |
1362 | // Wrap an optional error callback with a fallback error event.
1363 | Backbone.wrapError = function(onError, originalModel, options) {
1364 | return function(model, resp) {
1365 | resp = model === originalModel ? resp : model;
1366 | if (onError) {
1367 | onError(originalModel, resp, options);
1368 | } else {
1369 | originalModel.trigger('error', originalModel, resp, options);
1370 | }
1371 | };
1372 | };
1373 |
1374 | // Helpers
1375 | // -------
1376 |
1377 | // Shared empty constructor function to aid in prototype-chain creation.
1378 | var ctor = function(){};
1379 |
1380 | // Helper function to correctly set up the prototype chain, for subclasses.
1381 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
1382 | // class properties to be extended.
1383 | var inherits = function(parent, protoProps, staticProps) {
1384 | var child;
1385 |
1386 | // The constructor function for the new subclass is either defined by you
1387 | // (the "constructor" property in your `extend` definition), or defaulted
1388 | // by us to simply call the parent's constructor.
1389 | if (protoProps && protoProps.hasOwnProperty('constructor')) {
1390 | child = protoProps.constructor;
1391 | } else {
1392 | child = function(){ parent.apply(this, arguments); };
1393 | }
1394 |
1395 | // Inherit class (static) properties from parent.
1396 | _.extend(child, parent);
1397 |
1398 | // Set the prototype chain to inherit from `parent`, without calling
1399 | // `parent`'s constructor function.
1400 | ctor.prototype = parent.prototype;
1401 | child.prototype = new ctor();
1402 |
1403 | // Add prototype properties (instance properties) to the subclass,
1404 | // if supplied.
1405 | if (protoProps) _.extend(child.prototype, protoProps);
1406 |
1407 | // Add static properties to the constructor function, if supplied.
1408 | if (staticProps) _.extend(child, staticProps);
1409 |
1410 | // Correctly set child's `prototype.constructor`.
1411 | child.prototype.constructor = child;
1412 |
1413 | // Set a convenience property in case the parent's prototype is needed later.
1414 | child.__super__ = parent.prototype;
1415 |
1416 | return child;
1417 | };
1418 |
1419 | // Helper function to get a value from a Backbone object as a property
1420 | // or as a function.
1421 | var getValue = function(object, prop) {
1422 | if (!(object && object[prop])) return null;
1423 | return _.isFunction(object[prop]) ? object[prop]() : object[prop];
1424 | };
1425 |
1426 | // Throw an error when a URL is needed, and none is supplied.
1427 | var urlError = function() {
1428 | throw new Error('A "url" property or function must be specified');
1429 | };
1430 |
1431 | }).call(this);
1432 |
--------------------------------------------------------------------------------
/test/lib/qunit-1.10.0.css:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.10.0 - A JavaScript Unit Testing Framework
3 | *
4 | * http://qunitjs.com
5 | *
6 | * Copyright 2012 jQuery Foundation and other contributors
7 | * Released under the MIT license.
8 | * http://jquery.org/license
9 | */
10 |
11 | /** Font Family and Sizes */
12 |
13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
15 | }
16 |
17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
18 | #qunit-tests { font-size: smaller; }
19 |
20 |
21 | /** Resets */
22 |
23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 |
29 | /** Header */
30 |
31 | #qunit-header {
32 | padding: 0.5em 0 0.5em 1em;
33 |
34 | color: #8699a4;
35 | background-color: #0d3349;
36 |
37 | font-size: 1.5em;
38 | line-height: 1em;
39 | font-weight: normal;
40 |
41 | border-radius: 5px 5px 0 0;
42 | -moz-border-radius: 5px 5px 0 0;
43 | -webkit-border-top-right-radius: 5px;
44 | -webkit-border-top-left-radius: 5px;
45 | }
46 |
47 | #qunit-header a {
48 | text-decoration: none;
49 | color: #c2ccd1;
50 | }
51 |
52 | #qunit-header a:hover,
53 | #qunit-header a:focus {
54 | color: #fff;
55 | }
56 |
57 | #qunit-testrunner-toolbar label {
58 | display: inline-block;
59 | padding: 0 .5em 0 .1em;
60 | }
61 |
62 | #qunit-banner {
63 | height: 5px;
64 | }
65 |
66 | #qunit-testrunner-toolbar {
67 | padding: 0.5em 0 0.5em 2em;
68 | color: #5E740B;
69 | background-color: #eee;
70 | overflow: hidden;
71 | }
72 |
73 | #qunit-userAgent {
74 | padding: 0.5em 0 0.5em 2.5em;
75 | background-color: #2b81af;
76 | color: #fff;
77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
78 | }
79 |
80 | #qunit-modulefilter-container {
81 | float: right;
82 | }
83 |
84 | /** Tests: Pass/Fail */
85 |
86 | #qunit-tests {
87 | list-style-position: inside;
88 | }
89 |
90 | #qunit-tests li {
91 | padding: 0.4em 0.5em 0.4em 2.5em;
92 | border-bottom: 1px solid #fff;
93 | list-style-position: inside;
94 | }
95 |
96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
97 | display: none;
98 | }
99 |
100 | #qunit-tests li strong {
101 | cursor: pointer;
102 | }
103 |
104 | #qunit-tests li a {
105 | padding: 0.5em;
106 | color: #c2ccd1;
107 | text-decoration: none;
108 | }
109 | #qunit-tests li a:hover,
110 | #qunit-tests li a:focus {
111 | color: #000;
112 | }
113 |
114 | #qunit-tests ol {
115 | margin-top: 0.5em;
116 | padding: 0.5em;
117 |
118 | background-color: #fff;
119 |
120 | border-radius: 5px;
121 | -moz-border-radius: 5px;
122 | -webkit-border-radius: 5px;
123 | }
124 |
125 | #qunit-tests table {
126 | border-collapse: collapse;
127 | margin-top: .2em;
128 | }
129 |
130 | #qunit-tests th {
131 | text-align: right;
132 | vertical-align: top;
133 | padding: 0 .5em 0 0;
134 | }
135 |
136 | #qunit-tests td {
137 | vertical-align: top;
138 | }
139 |
140 | #qunit-tests pre {
141 | margin: 0;
142 | white-space: pre-wrap;
143 | word-wrap: break-word;
144 | }
145 |
146 | #qunit-tests del {
147 | background-color: #e0f2be;
148 | color: #374e0c;
149 | text-decoration: none;
150 | }
151 |
152 | #qunit-tests ins {
153 | background-color: #ffcaca;
154 | color: #500;
155 | text-decoration: none;
156 | }
157 |
158 | /*** Test Counts */
159 |
160 | #qunit-tests b.counts { color: black; }
161 | #qunit-tests b.passed { color: #5E740B; }
162 | #qunit-tests b.failed { color: #710909; }
163 |
164 | #qunit-tests li li {
165 | padding: 5px;
166 | background-color: #fff;
167 | border-bottom: none;
168 | list-style-position: inside;
169 | }
170 |
171 | /*** Passing Styles */
172 |
173 | #qunit-tests li li.pass {
174 | color: #3c510c;
175 | background-color: #fff;
176 | border-left: 10px solid #C6E746;
177 | }
178 |
179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
180 | #qunit-tests .pass .test-name { color: #366097; }
181 |
182 | #qunit-tests .pass .test-actual,
183 | #qunit-tests .pass .test-expected { color: #999999; }
184 |
185 | #qunit-banner.qunit-pass { background-color: #C6E746; }
186 |
187 | /*** Failing Styles */
188 |
189 | #qunit-tests li li.fail {
190 | color: #710909;
191 | background-color: #fff;
192 | border-left: 10px solid #EE5757;
193 | white-space: pre;
194 | }
195 |
196 | #qunit-tests > li:last-child {
197 | border-radius: 0 0 5px 5px;
198 | -moz-border-radius: 0 0 5px 5px;
199 | -webkit-border-bottom-right-radius: 5px;
200 | -webkit-border-bottom-left-radius: 5px;
201 | }
202 |
203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
204 | #qunit-tests .fail .test-name,
205 | #qunit-tests .fail .module-name { color: #000000; }
206 |
207 | #qunit-tests .fail .test-actual { color: #EE5757; }
208 | #qunit-tests .fail .test-expected { color: green; }
209 |
210 | #qunit-banner.qunit-fail { background-color: #EE5757; }
211 |
212 |
213 | /** Result */
214 |
215 | #qunit-testresult {
216 | padding: 0.5em 0.5em 0.5em 2.5em;
217 |
218 | color: #2b81af;
219 | background-color: #D2E0E6;
220 |
221 | border-bottom: 1px solid white;
222 | }
223 | #qunit-testresult .module-name {
224 | font-weight: bold;
225 | }
226 |
227 | /** Fixture */
228 |
229 | #qunit-fixture {
230 | position: absolute;
231 | top: -10000px;
232 | left: -10000px;
233 | width: 1000px;
234 | height: 1000px;
235 | }
236 |
--------------------------------------------------------------------------------
/test/lib/qunit-1.10.0.js:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.10.0 - A JavaScript Unit Testing Framework
3 | *
4 | * http://qunitjs.com
5 | *
6 | * Copyright 2012 jQuery Foundation and other contributors
7 | * Released under the MIT license.
8 | * http://jquery.org/license
9 | */
10 |
11 | (function( window ) {
12 |
13 | var QUnit,
14 | config,
15 | onErrorFnPrev,
16 | testId = 0,
17 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""),
18 | toString = Object.prototype.toString,
19 | hasOwn = Object.prototype.hasOwnProperty,
20 | // Keep a local reference to Date (GH-283)
21 | Date = window.Date,
22 | defined = {
23 | setTimeout: typeof window.setTimeout !== "undefined",
24 | sessionStorage: (function() {
25 | var x = "qunit-test-string";
26 | try {
27 | sessionStorage.setItem( x, x );
28 | sessionStorage.removeItem( x );
29 | return true;
30 | } catch( e ) {
31 | return false;
32 | }
33 | }())
34 | };
35 |
36 | function Test( settings ) {
37 | extend( this, settings );
38 | this.assertions = [];
39 | this.testNumber = ++Test.count;
40 | }
41 |
42 | Test.count = 0;
43 |
44 | Test.prototype = {
45 | init: function() {
46 | var a, b, li,
47 | tests = id( "qunit-tests" );
48 |
49 | if ( tests ) {
50 | b = document.createElement( "strong" );
51 | b.innerHTML = this.name;
52 |
53 | // `a` initialized at top of scope
54 | a = document.createElement( "a" );
55 | a.innerHTML = "Rerun";
56 | a.href = QUnit.url({ testNumber: this.testNumber });
57 |
58 | li = document.createElement( "li" );
59 | li.appendChild( b );
60 | li.appendChild( a );
61 | li.className = "running";
62 | li.id = this.id = "qunit-test-output" + testId++;
63 |
64 | tests.appendChild( li );
65 | }
66 | },
67 | setup: function() {
68 | if ( this.module !== config.previousModule ) {
69 | if ( config.previousModule ) {
70 | runLoggingCallbacks( "moduleDone", QUnit, {
71 | name: config.previousModule,
72 | failed: config.moduleStats.bad,
73 | passed: config.moduleStats.all - config.moduleStats.bad,
74 | total: config.moduleStats.all
75 | });
76 | }
77 | config.previousModule = this.module;
78 | config.moduleStats = { all: 0, bad: 0 };
79 | runLoggingCallbacks( "moduleStart", QUnit, {
80 | name: this.module
81 | });
82 | } else if ( config.autorun ) {
83 | runLoggingCallbacks( "moduleStart", QUnit, {
84 | name: this.module
85 | });
86 | }
87 |
88 | config.current = this;
89 |
90 | this.testEnvironment = extend({
91 | setup: function() {},
92 | teardown: function() {}
93 | }, this.moduleTestEnvironment );
94 |
95 | runLoggingCallbacks( "testStart", QUnit, {
96 | name: this.testName,
97 | module: this.module
98 | });
99 |
100 | // allow utility functions to access the current test environment
101 | // TODO why??
102 | QUnit.current_testEnvironment = this.testEnvironment;
103 |
104 | if ( !config.pollution ) {
105 | saveGlobal();
106 | }
107 | if ( config.notrycatch ) {
108 | this.testEnvironment.setup.call( this.testEnvironment );
109 | return;
110 | }
111 | try {
112 | this.testEnvironment.setup.call( this.testEnvironment );
113 | } catch( e ) {
114 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) );
115 | }
116 | },
117 | run: function() {
118 | config.current = this;
119 |
120 | var running = id( "qunit-testresult" );
121 |
122 | if ( running ) {
123 | running.innerHTML = "Running: " + this.name;
124 | }
125 |
126 | if ( this.async ) {
127 | QUnit.stop();
128 | }
129 |
130 | if ( config.notrycatch ) {
131 | this.callback.call( this.testEnvironment, QUnit.assert );
132 | return;
133 | }
134 |
135 | try {
136 | this.callback.call( this.testEnvironment, QUnit.assert );
137 | } catch( e ) {
138 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + e.message, extractStacktrace( e, 0 ) );
139 | // else next test will carry the responsibility
140 | saveGlobal();
141 |
142 | // Restart the tests if they're blocking
143 | if ( config.blocking ) {
144 | QUnit.start();
145 | }
146 | }
147 | },
148 | teardown: function() {
149 | config.current = this;
150 | if ( config.notrycatch ) {
151 | this.testEnvironment.teardown.call( this.testEnvironment );
152 | return;
153 | } else {
154 | try {
155 | this.testEnvironment.teardown.call( this.testEnvironment );
156 | } catch( e ) {
157 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) );
158 | }
159 | }
160 | checkPollution();
161 | },
162 | finish: function() {
163 | config.current = this;
164 | if ( config.requireExpects && this.expected == null ) {
165 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
166 | } else if ( this.expected != null && this.expected != this.assertions.length ) {
167 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
168 | } else if ( this.expected == null && !this.assertions.length ) {
169 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
170 | }
171 |
172 | var assertion, a, b, i, li, ol,
173 | test = this,
174 | good = 0,
175 | bad = 0,
176 | tests = id( "qunit-tests" );
177 |
178 | config.stats.all += this.assertions.length;
179 | config.moduleStats.all += this.assertions.length;
180 |
181 | if ( tests ) {
182 | ol = document.createElement( "ol" );
183 |
184 | for ( i = 0; i < this.assertions.length; i++ ) {
185 | assertion = this.assertions[i];
186 |
187 | li = document.createElement( "li" );
188 | li.className = assertion.result ? "pass" : "fail";
189 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" );
190 | ol.appendChild( li );
191 |
192 | if ( assertion.result ) {
193 | good++;
194 | } else {
195 | bad++;
196 | config.stats.bad++;
197 | config.moduleStats.bad++;
198 | }
199 | }
200 |
201 | // store result when possible
202 | if ( QUnit.config.reorder && defined.sessionStorage ) {
203 | if ( bad ) {
204 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad );
205 | } else {
206 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName );
207 | }
208 | }
209 |
210 | if ( bad === 0 ) {
211 | ol.style.display = "none";
212 | }
213 |
214 | // `b` initialized at top of scope
215 | b = document.createElement( "strong" );
216 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")";
217 |
218 | addEvent(b, "click", function() {
219 | var next = b.nextSibling.nextSibling,
220 | display = next.style.display;
221 | next.style.display = display === "none" ? "block" : "none";
222 | });
223 |
224 | addEvent(b, "dblclick", function( e ) {
225 | var target = e && e.target ? e.target : window.event.srcElement;
226 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
227 | target = target.parentNode;
228 | }
229 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
230 | window.location = QUnit.url({ testNumber: test.testNumber });
231 | }
232 | });
233 |
234 | // `li` initialized at top of scope
235 | li = id( this.id );
236 | li.className = bad ? "fail" : "pass";
237 | li.removeChild( li.firstChild );
238 | a = li.firstChild;
239 | li.appendChild( b );
240 | li.appendChild ( a );
241 | li.appendChild( ol );
242 |
243 | } else {
244 | for ( i = 0; i < this.assertions.length; i++ ) {
245 | if ( !this.assertions[i].result ) {
246 | bad++;
247 | config.stats.bad++;
248 | config.moduleStats.bad++;
249 | }
250 | }
251 | }
252 |
253 | runLoggingCallbacks( "testDone", QUnit, {
254 | name: this.testName,
255 | module: this.module,
256 | failed: bad,
257 | passed: this.assertions.length - bad,
258 | total: this.assertions.length
259 | });
260 |
261 | QUnit.reset();
262 |
263 | config.current = undefined;
264 | },
265 |
266 | queue: function() {
267 | var bad,
268 | test = this;
269 |
270 | synchronize(function() {
271 | test.init();
272 | });
273 | function run() {
274 | // each of these can by async
275 | synchronize(function() {
276 | test.setup();
277 | });
278 | synchronize(function() {
279 | test.run();
280 | });
281 | synchronize(function() {
282 | test.teardown();
283 | });
284 | synchronize(function() {
285 | test.finish();
286 | });
287 | }
288 |
289 | // `bad` initialized at top of scope
290 | // defer when previous test run passed, if storage is available
291 | bad = QUnit.config.reorder && defined.sessionStorage &&
292 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );
293 |
294 | if ( bad ) {
295 | run();
296 | } else {
297 | synchronize( run, true );
298 | }
299 | }
300 | };
301 |
302 | // Root QUnit object.
303 | // `QUnit` initialized at top of scope
304 | QUnit = {
305 |
306 | // call on start of module test to prepend name to all tests
307 | module: function( name, testEnvironment ) {
308 | config.currentModule = name;
309 | config.currentModuleTestEnvironment = testEnvironment;
310 | config.modules[name] = true;
311 | },
312 |
313 | asyncTest: function( testName, expected, callback ) {
314 | if ( arguments.length === 2 ) {
315 | callback = expected;
316 | expected = null;
317 | }
318 |
319 | QUnit.test( testName, expected, callback, true );
320 | },
321 |
322 | test: function( testName, expected, callback, async ) {
323 | var test,
324 | name = "" + escapeInnerText( testName ) + "";
325 |
326 | if ( arguments.length === 2 ) {
327 | callback = expected;
328 | expected = null;
329 | }
330 |
331 | if ( config.currentModule ) {
332 | name = "" + config.currentModule + ": " + name;
333 | }
334 |
335 | test = new Test({
336 | name: name,
337 | testName: testName,
338 | expected: expected,
339 | async: async,
340 | callback: callback,
341 | module: config.currentModule,
342 | moduleTestEnvironment: config.currentModuleTestEnvironment,
343 | stack: sourceFromStacktrace( 2 )
344 | });
345 |
346 | if ( !validTest( test ) ) {
347 | return;
348 | }
349 |
350 | test.queue();
351 | },
352 |
353 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
354 | expect: function( asserts ) {
355 | if (arguments.length === 1) {
356 | config.current.expected = asserts;
357 | } else {
358 | return config.current.expected;
359 | }
360 | },
361 |
362 | start: function( count ) {
363 | config.semaphore -= count || 1;
364 | // don't start until equal number of stop-calls
365 | if ( config.semaphore > 0 ) {
366 | return;
367 | }
368 | // ignore if start is called more often then stop
369 | if ( config.semaphore < 0 ) {
370 | config.semaphore = 0;
371 | }
372 | // A slight delay, to avoid any current callbacks
373 | if ( defined.setTimeout ) {
374 | window.setTimeout(function() {
375 | if ( config.semaphore > 0 ) {
376 | return;
377 | }
378 | if ( config.timeout ) {
379 | clearTimeout( config.timeout );
380 | }
381 |
382 | config.blocking = false;
383 | process( true );
384 | }, 13);
385 | } else {
386 | config.blocking = false;
387 | process( true );
388 | }
389 | },
390 |
391 | stop: function( count ) {
392 | config.semaphore += count || 1;
393 | config.blocking = true;
394 |
395 | if ( config.testTimeout && defined.setTimeout ) {
396 | clearTimeout( config.timeout );
397 | config.timeout = window.setTimeout(function() {
398 | QUnit.ok( false, "Test timed out" );
399 | config.semaphore = 1;
400 | QUnit.start();
401 | }, config.testTimeout );
402 | }
403 | }
404 | };
405 |
406 | // Asssert helpers
407 | // All of these must call either QUnit.push() or manually do:
408 | // - runLoggingCallbacks( "log", .. );
409 | // - config.current.assertions.push({ .. });
410 | QUnit.assert = {
411 | /**
412 | * Asserts rough true-ish result.
413 | * @name ok
414 | * @function
415 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
416 | */
417 | ok: function( result, msg ) {
418 | if ( !config.current ) {
419 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) );
420 | }
421 | result = !!result;
422 |
423 | var source,
424 | details = {
425 | module: config.current.module,
426 | name: config.current.testName,
427 | result: result,
428 | message: msg
429 | };
430 |
431 | msg = escapeInnerText( msg || (result ? "okay" : "failed" ) );
432 | msg = "" + msg + "";
433 |
434 | if ( !result ) {
435 | source = sourceFromStacktrace( 2 );
436 | if ( source ) {
437 | details.source = source;
438 | msg += "
Source:
" + escapeInnerText( source ) + "
";
439 | }
440 | }
441 | runLoggingCallbacks( "log", QUnit, details );
442 | config.current.assertions.push({
443 | result: result,
444 | message: msg
445 | });
446 | },
447 |
448 | /**
449 | * Assert that the first two arguments are equal, with an optional message.
450 | * Prints out both actual and expected values.
451 | * @name equal
452 | * @function
453 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
454 | */
455 | equal: function( actual, expected, message ) {
456 | QUnit.push( expected == actual, actual, expected, message );
457 | },
458 |
459 | /**
460 | * @name notEqual
461 | * @function
462 | */
463 | notEqual: function( actual, expected, message ) {
464 | QUnit.push( expected != actual, actual, expected, message );
465 | },
466 |
467 | /**
468 | * @name deepEqual
469 | * @function
470 | */
471 | deepEqual: function( actual, expected, message ) {
472 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
473 | },
474 |
475 | /**
476 | * @name notDeepEqual
477 | * @function
478 | */
479 | notDeepEqual: function( actual, expected, message ) {
480 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
481 | },
482 |
483 | /**
484 | * @name strictEqual
485 | * @function
486 | */
487 | strictEqual: function( actual, expected, message ) {
488 | QUnit.push( expected === actual, actual, expected, message );
489 | },
490 |
491 | /**
492 | * @name notStrictEqual
493 | * @function
494 | */
495 | notStrictEqual: function( actual, expected, message ) {
496 | QUnit.push( expected !== actual, actual, expected, message );
497 | },
498 |
499 | throws: function( block, expected, message ) {
500 | var actual,
501 | ok = false;
502 |
503 | // 'expected' is optional
504 | if ( typeof expected === "string" ) {
505 | message = expected;
506 | expected = null;
507 | }
508 |
509 | config.current.ignoreGlobalErrors = true;
510 | try {
511 | block.call( config.current.testEnvironment );
512 | } catch (e) {
513 | actual = e;
514 | }
515 | config.current.ignoreGlobalErrors = false;
516 |
517 | if ( actual ) {
518 | // we don't want to validate thrown error
519 | if ( !expected ) {
520 | ok = true;
521 | // expected is a regexp
522 | } else if ( QUnit.objectType( expected ) === "regexp" ) {
523 | ok = expected.test( actual );
524 | // expected is a constructor
525 | } else if ( actual instanceof expected ) {
526 | ok = true;
527 | // expected is a validation function which returns true is validation passed
528 | } else if ( expected.call( {}, actual ) === true ) {
529 | ok = true;
530 | }
531 |
532 | QUnit.push( ok, actual, null, message );
533 | } else {
534 | QUnit.pushFailure( message, null, 'No exception was thrown.' );
535 | }
536 | }
537 | };
538 |
539 | /**
540 | * @deprecate since 1.8.0
541 | * Kept assertion helpers in root for backwards compatibility
542 | */
543 | extend( QUnit, QUnit.assert );
544 |
545 | /**
546 | * @deprecated since 1.9.0
547 | * Kept global "raises()" for backwards compatibility
548 | */
549 | QUnit.raises = QUnit.assert.throws;
550 |
551 | /**
552 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0
553 | * Kept to avoid TypeErrors for undefined methods.
554 | */
555 | QUnit.equals = function() {
556 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" );
557 | };
558 | QUnit.same = function() {
559 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" );
560 | };
561 |
562 | // We want access to the constructor's prototype
563 | (function() {
564 | function F() {}
565 | F.prototype = QUnit;
566 | QUnit = new F();
567 | // Make F QUnit's constructor so that we can add to the prototype later
568 | QUnit.constructor = F;
569 | }());
570 |
571 | /**
572 | * Config object: Maintain internal state
573 | * Later exposed as QUnit.config
574 | * `config` initialized at top of scope
575 | */
576 | config = {
577 | // The queue of tests to run
578 | queue: [],
579 |
580 | // block until document ready
581 | blocking: true,
582 |
583 | // when enabled, show only failing tests
584 | // gets persisted through sessionStorage and can be changed in UI via checkbox
585 | hidepassed: false,
586 |
587 | // by default, run previously failed tests first
588 | // very useful in combination with "Hide passed tests" checked
589 | reorder: true,
590 |
591 | // by default, modify document.title when suite is done
592 | altertitle: true,
593 |
594 | // when enabled, all tests must call expect()
595 | requireExpects: false,
596 |
597 | // add checkboxes that are persisted in the query-string
598 | // when enabled, the id is set to `true` as a `QUnit.config` property
599 | urlConfig: [
600 | {
601 | id: "noglobals",
602 | label: "Check for Globals",
603 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings."
604 | },
605 | {
606 | id: "notrycatch",
607 | label: "No try-catch",
608 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings."
609 | }
610 | ],
611 |
612 | // Set of all modules.
613 | modules: {},
614 |
615 | // logging callback queues
616 | begin: [],
617 | done: [],
618 | log: [],
619 | testStart: [],
620 | testDone: [],
621 | moduleStart: [],
622 | moduleDone: []
623 | };
624 |
625 | // Initialize more QUnit.config and QUnit.urlParams
626 | (function() {
627 | var i,
628 | location = window.location || { search: "", protocol: "file:" },
629 | params = location.search.slice( 1 ).split( "&" ),
630 | length = params.length,
631 | urlParams = {},
632 | current;
633 |
634 | if ( params[ 0 ] ) {
635 | for ( i = 0; i < length; i++ ) {
636 | current = params[ i ].split( "=" );
637 | current[ 0 ] = decodeURIComponent( current[ 0 ] );
638 | // allow just a key to turn on a flag, e.g., test.html?noglobals
639 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
640 | urlParams[ current[ 0 ] ] = current[ 1 ];
641 | }
642 | }
643 |
644 | QUnit.urlParams = urlParams;
645 |
646 | // String search anywhere in moduleName+testName
647 | config.filter = urlParams.filter;
648 |
649 | // Exact match of the module name
650 | config.module = urlParams.module;
651 |
652 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null;
653 |
654 | // Figure out if we're running the tests from a server or not
655 | QUnit.isLocal = location.protocol === "file:";
656 | }());
657 |
658 | // Export global variables, unless an 'exports' object exists,
659 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script)
660 | if ( typeof exports === "undefined" ) {
661 | extend( window, QUnit );
662 |
663 | // Expose QUnit object
664 | window.QUnit = QUnit;
665 | }
666 |
667 | // Extend QUnit object,
668 | // these after set here because they should not be exposed as global functions
669 | extend( QUnit, {
670 | config: config,
671 |
672 | // Initialize the configuration options
673 | init: function() {
674 | extend( config, {
675 | stats: { all: 0, bad: 0 },
676 | moduleStats: { all: 0, bad: 0 },
677 | started: +new Date(),
678 | updateRate: 1000,
679 | blocking: false,
680 | autostart: true,
681 | autorun: false,
682 | filter: "",
683 | queue: [],
684 | semaphore: 0
685 | });
686 |
687 | var tests, banner, result,
688 | qunit = id( "qunit" );
689 |
690 | if ( qunit ) {
691 | qunit.innerHTML =
692 | "