From b6ab336e97facfaa5596aa4eaacbe2b5ddf9c44e Mon Sep 17 00:00:00 2001 From: Simon Elliott Date: Fri, 27 Apr 2012 11:57:27 +0100 Subject: [PATCH] vendor stuff --- .npmignore | 4 + app/config.js | 30 + app/main.js | 81 + app/modules/example.js | 44 + app/namespace.js | 65 + app/templates/example.html | 355 + assets/css/index.css | 1 + assets/css/style.css | 342 + assets/img/backbone.png | Bin 0 -> 22740 bytes assets/js/libs/almond.js | 277 + assets/js/libs/backbone.js | 1431 ++++ assets/js/libs/crafty.js | 8976 ++++++++++++++++++++++ assets/js/libs/jquery.js | 9266 +++++++++++++++++++++++ assets/js/libs/require.js | 2053 +++++ assets/js/libs/underscore.js | 999 +++ assets/js/libs/xdate.js | 18 + assets/js/plugins/use.js | 102 + favicon.ico | Bin 0 -> 1150 bytes grunt.js | 148 + index.html | 21 + test/jasmine/index.html | 44 + test/jasmine/spec/example.js | 73 + test/jasmine/vendor/MIT.LICENSE | 20 + test/jasmine/vendor/jasmine-html.js | 190 + test/jasmine/vendor/jasmine.css | 166 + test/jasmine/vendor/jasmine.js | 2476 ++++++ test/jasmine/vendor/jasmine_favicon.png | Bin 0 -> 905 bytes test/qunit/index.html | 47 + test/qunit/tests/example.js | 54 + test/qunit/vendor/qunit.css | 228 + test/qunit/vendor/qunit.js | 1589 ++++ 31 files changed, 29100 insertions(+) create mode 100644 .npmignore create mode 100644 app/config.js create mode 100644 app/main.js create mode 100644 app/modules/example.js create mode 100644 app/namespace.js create mode 100644 app/templates/example.html create mode 100644 assets/css/index.css create mode 100644 assets/css/style.css create mode 100644 assets/img/backbone.png create mode 100644 assets/js/libs/almond.js create mode 100644 assets/js/libs/backbone.js create mode 100644 assets/js/libs/crafty.js create mode 100644 assets/js/libs/jquery.js create mode 100644 assets/js/libs/require.js create mode 100644 assets/js/libs/underscore.js create mode 100644 assets/js/libs/xdate.js create mode 100644 assets/js/plugins/use.js create mode 100644 favicon.ico create mode 100644 grunt.js create mode 100644 index.html create mode 100644 test/jasmine/index.html create mode 100644 test/jasmine/spec/example.js create mode 100644 test/jasmine/vendor/MIT.LICENSE create mode 100644 test/jasmine/vendor/jasmine-html.js create mode 100644 test/jasmine/vendor/jasmine.css create mode 100644 test/jasmine/vendor/jasmine.js create mode 100644 test/jasmine/vendor/jasmine_favicon.png create mode 100644 test/qunit/index.html create mode 100644 test/qunit/tests/example.js create mode 100644 test/qunit/vendor/qunit.css create mode 100644 test/qunit/vendor/qunit.js diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ee2f8fe --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +dist/ +npm-debug.log +.lock-wscript +build/node_modules/grunt/node_modules/glob/build diff --git a/app/config.js b/app/config.js new file mode 100644 index 0000000..8412b8c --- /dev/null +++ b/app/config.js @@ -0,0 +1,30 @@ +// Set the require.js configuration for your application. +require.config({ + // Initialize the application with the main application file + deps: ["main"], + + paths: { + // JavaScript folders + libs: "../assets/js/libs", + plugins: "../assets/js/plugins", + + // Libraries + jquery: "../assets/js/libs/jquery", + underscore: "../assets/js/libs/underscore", + backbone: "../assets/js/libs/backbone", + + // Shim Plugin + use: "../assets/js/plugins/use" + }, + + use: { + backbone: { + deps: ["use!underscore", "jquery"], + attach: "Backbone" + }, + + underscore: { + attach: "_" + } + } +}); diff --git a/app/main.js b/app/main.js new file mode 100644 index 0000000..4c1c2ea --- /dev/null +++ b/app/main.js @@ -0,0 +1,81 @@ +require([ + "namespace", + + // Libs + "jquery", + "use!backbone", + + // Modules + "modules/example" +], + +function(namespace, $, Backbone, Example) { + + // Defining the application router, you can attach sub routers here. + var Router = Backbone.Router.extend({ + routes: { + "": "index", + ":hash": "index" + }, + + index: function(hash) { + var route = this; + var tutorial = new Example.Views.Tutorial(); + + // Attach the tutorial to the DOM + tutorial.render(function(el) { + $("#main").html(el); + + // Fix for hashes in pushState and hash fragment + if (hash && !route._alreadyTriggered) { + // Reset to home, pushState support automatically converts hashes + Backbone.history.navigate("", false); + + // Trigger the default browser behavior + location.hash = hash; + + // Set an internal flag to stop recursive looping + route._alreadyTriggered = true; + } + }); + } + }); + + // Shorthand the application namespace + var app = namespace.app; + + // Treat the jQuery ready function as the entry point to the application. + // Inside this function, kick-off all initialization, everything up to this + // point should be definitions. + $(function() { + // Define your master router on the application namespace and trigger all + // navigation from this instance. + app.router = new Router(); + + // Trigger the initial route and enable HTML5 History API support + Backbone.history.start({ pushState: true }); + }); + + // All navigation that is relative should be passed through the navigate + // method, to be processed by the router. If the link has a data-bypass + // attribute, bypass the delegation completely. + $(document).on("click", "a:not([data-bypass])", function(evt) { + // Get the anchor href and protcol + var href = $(this).attr("href"); + var protocol = this.protocol + "//"; + + // Ensure the protocol is not part of URL, meaning its relative. + if (href && href.slice(0, protocol.length) !== protocol && + href.indexOf("javascript:") !== 0) { + // Stop the default event to ensure the link will not cause a page + // refresh. + evt.preventDefault(); + + // `Backbone.history.navigate` is sufficient for all Routers and will + // trigger the correct events. The Router's internal `navigate` method + // calls this anyways. + Backbone.history.navigate(href, true); + } + }); + +}); diff --git a/app/modules/example.js b/app/modules/example.js new file mode 100644 index 0000000..9a42a81 --- /dev/null +++ b/app/modules/example.js @@ -0,0 +1,44 @@ +define([ + "namespace", + + // Libs + "use!backbone" + + // Modules + + // Plugins +], + +function(namespace, Backbone) { + + // Create a new module + var Example = namespace.module(); + + // Example extendings + Example.Model = Backbone.Model.extend({ /* ... */ }); + Example.Collection = Backbone.Collection.extend({ /* ... */ }); + Example.Router = Backbone.Router.extend({ /* ... */ }); + + // This will fetch the tutorial template and render it. + Example.Views.Tutorial = Backbone.View.extend({ + template: "app/templates/example.html", + + render: function(done) { + var view = this; + + // Fetch the template, render it to the View element and call done. + namespace.fetchTemplate(this.template, function(tmpl) { + view.el.innerHTML = tmpl(); + + // If a done function is passed, call it with the element + if (_.isFunction(done)) { + done(view.el); + } + }); + } + }); + + // Required, return the module for AMD compliance + return Example; + +}); diff --git a/app/namespace.js b/app/namespace.js new file mode 100644 index 0000000..17e380d --- /dev/null +++ b/app/namespace.js @@ -0,0 +1,65 @@ +define([ + // Libs + "jquery", + "use!underscore", + "use!backbone" +], + +function($, _, Backbone) { + // Put application wide code here + + return { + // This is useful when developing if you don't want to use a + // build process every time you change a template. + // + // Delete if you are using a different template loading method. + fetchTemplate: function(path, done) { + var JST = window.JST = window.JST || {}; + var def = new $.Deferred(); + + // Should be an instant synchronous way of getting the template, if it + // exists in the JST object. + if (JST[path]) { + if (_.isFunction(done)) { + done(JST[path]); + } + + return def.resolve(JST[path]); + } + + // Fetch it asynchronously if not available from JST, ensure that + // template requests are never cached and prevent global ajax event + // handlers from firing. + $.ajax({ + url: path, + type: "get", + dataType: "text", + cache: false, + global: false, + + success: function(contents) { + JST[path] = _.template(contents); + + // Set the global JST cache and return the template + if (_.isFunction(done)) { + done(JST[path]); + } + + // Resolve the template deferred + def.resolve(JST[path]); + } + }); + + // Ensure a normalized return value (Promise) + return def.promise(); + }, + + // Create a custom object with a nested Views object + module: function(additionalProps) { + return _.extend({ Views: {} }, additionalProps); + }, + + // Keep active application instances namespaced under an app object. + app: _.extend({}, Backbone.Events) + }; +}); diff --git a/app/templates/example.html b/app/templates/example.html new file mode 100644 index 0000000..e547fff --- /dev/null +++ b/app/templates/example.html @@ -0,0 +1,355 @@ +
+ +
+ +
+

Congratulations!

+

Seeing this means you have installed Backbone Boilerplate correctly.

+ +

Now that you have the easiest and most-powerful Backbone boilerplate available, + you're probably wondering how to use it to start building applications...

+
+
+ +
+

Contents

+ + +
+ +
+

Overview

+ +

Backbone Boilerplate is the product of much research and frustration. While + existing boilerplates for Backbone exist, they will often modify the Backbone + core, don't have an integrated build system, or impose too much on your + application's structure. This boilerplate attempts to improve that. + + Organize your application in a logical filesystem, and develop Models, + Collections, Views, and Routers inside modules. Build your application knowing + you have efficient, compact code. Backbone Boilerplate extends on the + versatile Backbone core, and helps developers manage their application.

+ +

Core Features

+ +
+ +
+

Getting help

+ +

If you're encountering issues, need assistance, or have a question that hasn't been answered in this + tutorial or the GitHub project page + you may find help in one of these places:

+ + + +

I want this project to be the best it possibly can and represent the interests of the community, please + submit issues with features you find useful and anything that you question.

+
+ +
+

Writing your application

+

Your application may be made up of third-party libraries, plugins, application code, templates, and lots of logic. All of this will need + to be well structured to keep it maintainable and it also needs to be compiled if deployed into production. Before you can get started you + will need to clean out all the existing defaults that are in the boilerplate are necessary to display this tutorial. +

+ +

Strongly recommend you read through this tutorial before cleaning out any files that may hold clues on how to use the Boilerplate.

+ +

Cleaning out default files and code

+

There are several places where customization may be required.

+ + + +

Namespace

+

The namespace.js file is very important since it contains logic that should + exist for every module in your application. This also contains the module + shortcut function and fetchTemplate function.

+

+ +

Creating a module

+

Following the Bocoup post on Organizing Your Backbone.js Application With Modules this boilerplate provides the same module definition structure. + + Modules are placed in the app/modules/ directory. There is an example module + there named: example.js. The actual module definition function is located + inside the app/index.js file. You create and reference modules with the same + function call: namespace.module("<module_name>"). + + Typically a module contains a single Model/Collection/Router and many Views. + Therefore the returned module object is empty except for a Views object + property that can be used to attach many Views to, like: + +


+  MyModule.Views.Detailed = Backbone.View.extend({ /* ... */ });
+
+  MyModule.Views.Main = Backbone.View.extend({ /* ... */ });
+  
+

+ +

+ Attaching Models/Collections/Routers happen on the same level of the module, + like so: + +


+  MyModule.Model = Backbone.Model.extend({ /* ... */ });
+
+  MyModule.Router = Backbone.Router.extend({ /* ... */ });
+  
+

+ +

Working with templates

+

Templates are a super useful way to separate concerns in your application. Instead of generating markup from inside your JavaScript + application, you instead create it in a separate file and load it into your application. There are numerous ways of loading in a + template, but this boilerplate has chosen the most performant way to build all your templates into a single file.

+ +

This tutorial itself is a template that exists in app/templates/example.html. You can edit this file and hit refresh + in here to see the changes. The boilerplate comes with a built in function to handle the loading of templates. It's called: + +


+  namespace.fetchTemplate("app/templates/name.html", function(template) {
+    // Template here is a function, that accepts an object.  Identical to _.template.
+    console.log(template({ ... }));
+  });
+  
+

+ +

By defining a custom function this will ensure that if you use the build tool or AJAX, that your templates will load consistently. + You can see it in action inside the app/modules/example.js module.

+ +

If you use the build process to compile your templates, it will automatically find all the HTML files inside the templates + directory and compile them into a templates.js file. These are actual JavaScript template functions being compiled on the server, which + is different from Jammit and most other server-side builders that just invoke functions on page load.

+ +

You can access a compiled template like so: + +


+  var template = window.JST["app/modules/example.html"];
+  template({ ... });
+  
+

+ +

Working with Application Wide Events

+

Application wide events provide a convenient way for modules to communicate with each other. namespace.app references a copy of the Backbone.Events object. More information on Backbone Events

+ +

+ To bind a callback function to an event: + +


+  //binding an annonymous function to the event "all" with a context of this.
+  namespace.app.on("all", function(){...}, this);
+  
+

+ +

+ To remove a callback function (or many) from an event: + +


+  // Removes just the doSomething callback.
+  namespace.app.off("change", doSomething);
+  
+  // Removes all "change" events.
+  namespace.app.off("change");
+  
+  // Removes all events from the namespace.app object. 
+  namespace.app.off(); 
+  
+

+ +

+ To trigger the "change" event: + +


+  namespace.app.trigger("change", [*args]);
+  
+

+ +

Working with libraries and plugins

+

Libraries and plugins are easily added to the application, by placing them inside the assets/js/libs/ directory. + If you have many plugins in your application, it may make sense to create a separate folder such as assets/js/plugins/ + for them.

+
+ +
+

Using the build tool

+

The Backbone Boilerplate build process is a state-of-the-art task driven + Node.js application that utilizes @cowboy's grunt project. + + To run the defaults, execute the following command from the project root, + and *not from inside the build folder*.

+ +

Running with the defaults

+

To run the defaults, execute the following command from the project root, + and *not from inside the build folder*. + +


+  node build
+  
+

+ +

+ This will do a number of things for you. First it will concatenate all your + libs, app code, and templates into separate files inside the `dist/debug` + folder. It will then minify those files and your CSS into production ready + files inside the dist/release folder.

+ +

Customizing the build configuration

+

To customize and configure the build tool, open `build/config.js` and tweak +the settings.

+ +

Using the development server

+

+ While writing an application that leverages pushState you can run the + following command to run a server that will always resolve to the index.html + +


+  node build/server
+  
+

+ +

+ This will spawn up an HTTP server on port 8000. This server is intended + for development and not production. You should use url rewriting or forwarding + all requests in your production server to achieve this same effect.

+ +

Serving the built assets

+ +

If you are using the build tool in conjunction with this development server + you can optionally update the index.html file to remove the existing script + tags and uncomment out the scripts tag at the bottom to load the dist/debug + or dist/release assets. You can achieve this by specifying either debug + or release after the server command, like so: + +


+  node build/server release
+  
+

+ +

Adding new tasks

+

To add a new task into the build system, you simply copy and paste the task JavaScript folder/file into the build/tasks folder + or extract the task archive into the same directory. At the very least in order to run this task, you'll need to add it to the build/config.js + file. The last line should look something like: + +


+  task.registerTask("default", "clean lint:files concat jst min mincss new_module_here");
+  
+

+ + It's possible the custom task will have additional setup instructions, so make + sure you read the README for any task.

+
+ +
+

Useful resources

+ + +
diff --git a/assets/css/index.css b/assets/css/index.css new file mode 100644 index 0000000..a70db9f --- /dev/null +++ b/assets/css/index.css @@ -0,0 +1 @@ +@import "style.css"; diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..c9726da --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,342 @@ +/* + * HTML5 ✰ Boilerplate + * + * What follows is the result of much research on cross-browser styling. + * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, + * Kroc Camen, and the H5BP dev community and team. + * + * Detailed information about this CSS: h5bp.com/css + * + * ==|== normalize ========================================================== + */ + + +/* ============================================================================= + HTML5 display definitions + ========================================================================== */ + +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } +audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } +audio:not([controls]) { display: none; } +[hidden] { display: none; } + + +/* ============================================================================= + Base + ========================================================================== */ + +/* + * 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units + * 2. Force vertical scrollbar in non-IE + * 3. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g + */ + +html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + +body { margin: 0; font-size: 1em; line-height: 1.4; } + +body, button, input, select, textarea { font-family: sans-serif; color: #222; } + +/* + * Remove text-shadow in selection highlight: h5bp.com/i + * These selection declarations have to be separate + * Also: hot pink! (or customize the background color to match your design) + */ + +::-moz-selection { background: #fe57a1; color: #fff; text-shadow: none; } +::selection { background: #fe57a1; color: #fff; text-shadow: none; } + + +/* ============================================================================= + Links + ========================================================================== */ + +a { color: #00e; } +a:visited { color: #551a8b; } +a:hover { color: #06e; } +a:focus { outline: thin dotted; } + +/* Improve readability when focused and hovered in all browsers: h5bp.com/h */ +a:hover, a:active { outline: 0; } + + +/* ============================================================================= + Typography + ========================================================================== */ + +abbr[title] { border-bottom: 1px dotted; } + +b, strong { font-weight: bold; } + +blockquote { margin: 1em 40px; } + +dfn { font-style: italic; } + +hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } + +ins { background: #ff9; color: #000; text-decoration: none; } + +mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } + +/* Redeclare monospace font family: h5bp.com/j */ +pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; } + +/* Improve readability of pre-formatted text in all browsers */ +pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } + +q { quotes: none; } +q:before, q:after { content: ""; content: none; } + +small { font-size: 85%; } + +/* Position subscript and superscript content without affecting line-height: h5bp.com/k */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } +sup { top: -0.5em; } +sub { bottom: -0.25em; } + + +/* ============================================================================= + Lists + ========================================================================== */ + +ul, ol { margin: 1em 0; padding: 0 0 0 40px; } +dd { margin: 0 0 0 40px; } +nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; } + + +/* ============================================================================= + Embedded content + ========================================================================== */ + +/* + * 1. Improve image quality when scaled in IE7: h5bp.com/d + * 2. Remove the gap between images and borders on image containers: h5bp.com/e + */ + +img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } + +/* + * Correct overflow not hidden in IE9 + */ + +svg:not(:root) { overflow: hidden; } + + +/* ============================================================================= + Figures + ========================================================================== */ + +figure { margin: 0; } + + +/* ============================================================================= + Forms + ========================================================================== */ + +form { margin: 0; } +fieldset { border: 0; margin: 0; padding: 0; } + +/* Indicate that 'label' will shift focus to the associated form element */ +label { cursor: pointer; } + +/* + * 1. Correct color not inheriting in IE6/7/8/9 + * 2. Correct alignment displayed oddly in IE6/7 + */ + +legend { border: 0; *margin-left: -7px; padding: 0; } + +/* + * 1. Correct font-size not inheriting in all browsers + * 2. Remove margins in FF3/4 S5 Chrome + * 3. Define consistent vertical alignment display in all browsers + */ + +button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; } + +/* + * 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet) + */ + +button, input { line-height: normal; } + +/* + * 1. Display hand cursor for clickable form elements + * 2. Allow styling of clickable form elements in iOS + * 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6) + */ + +button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; } + +/* + * Consistent box sizing and appearance + */ + +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; } +input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; } +input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/* + * Remove inner padding and border in FF3/4: h5bp.com/l + */ + +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/* + * 1. Remove default vertical scrollbar in IE6/7/8/9 + * 2. Allow only vertical resizing + */ + +textarea { overflow: auto; vertical-align: top; resize: vertical; } + +/* Colors for form validity */ +input:valid, textarea:valid { } +input:invalid, textarea:invalid { background-color: #f0dddd; } + + +/* ============================================================================= + Tables + ========================================================================== */ + +table { border-collapse: collapse; border-spacing: 0; } +td { vertical-align: top; } + + +/* ==|== primary styles ===================================================== + Author: Backbone Boilerplate + ========================================================================== */ + +#main { + margin: 40px; + font-size: 14px; +} + +#main pre { + min-width: 320px; + border-radius: 10px; + color: #333; + font-weight: normal; + font-size: 12px; + display: inline-block; + padding-right: 40px; + background: #F8FAFC; +} + +#main a { + color: #00CCFF; +} + +#main a:hover { + color: #FE57A1; +} + +#main img { + width: 320px; + height: auto; +} + +#main p { + line-height: 28px; +} + +#main ul.nested { + margin-top: 0; + margin-bottom: 0; +} + +#main ul li { + line-height: 28px; +} + +#main header h1 { + margin: 15px; + display: inline-block; + color: #333; +} + +#main header h2:not(.congrats) { + color: #666; + margin: 0; padding: 0; +} + +#main header .congrats { + padding-bottom: 0; + margin-bottom: 0; + color: #00CCFF; + display: inline-block; + font-size: 28px; +} + +#main .topic { + color: #CCC; +} + +#main .clues { + color: #FE57A1; +} + + +/* ==|== media queries ====================================================== + PLACEHOLDER Media Queries for Responsive Design. + These override the primary ('mobile first') styles + Modify as content requires. + ========================================================================== */ + +@media only screen and (min-width: 480px) { + /* Style adjustments for viewports 480px and over go here */ + +} + +@media only screen and (min-width: 768px) { + /* Style adjustments for viewports 768px and over go here */ + +} + + + +/* ==|== non-semantic helper classes ======================================== + Please define your styles before this section. + ========================================================================== */ + +/* For image replacement */ +.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; *line-height: 0; } +.ir br { display: none; } + +/* Hide from both screenreaders and browsers: h5bp.com/u */ +.hidden { display: none !important; visibility: hidden; } + +/* Hide only visually, but have it available for screenreaders: h5bp.com/v */ +.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } + +/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */ +.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } + +/* Hide visually and from screenreaders, but maintain layout */ +.invisible { visibility: hidden; } + +/* Contain floats: h5bp.com/q */ +.clearfix:before, .clearfix:after { content: ""; display: table; } +.clearfix:after { clear: both; } +.clearfix { *zoom: 1; } + + + +/* ==|== print styles ======================================================= + Print styles. + Inlined to avoid required HTTP connection: h5bp.com/r + ========================================================================== */ + +@media print { + * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */ + a, a:visited { text-decoration: underline; } + a[href]:after { content: " (" attr(href) ")"; } + abbr[title]:after { content: " (" attr(title) ")"; } + .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */ + pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } + thead { display: table-header-group; } /* h5bp.com/t */ + tr, img { page-break-inside: avoid; } + img { max-width: 100% !important; } + @page { margin: 0.5cm; } + p, h2, h3 { orphans: 3; widows: 3; } + h2, h3 { page-break-after: avoid; } +} diff --git a/assets/img/backbone.png b/assets/img/backbone.png new file mode 100644 index 0000000000000000000000000000000000000000..e12c5b63462e124d8e7a9a50435abf6081f1b4ad GIT binary patch literal 22740 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 0) { + name.splice(i - 1, 2); + i -= 2; + } + } + } + //end trimDots + + name = name.join("/"); + } + } + return name; + } + + function makeRequire(relName, forceSync) { + return function () { + //A version of a require function that passes a moduleName + //value for items that may need to + //look up paths relative to the moduleName + return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync])); + }; + } + + function makeNormalize(relName) { + return function (name) { + return normalize(name, relName); + }; + } + + function makeLoad(depName) { + return function (value) { + defined[depName] = value; + }; + } + + function callDep(name) { + if (waiting.hasOwnProperty(name)) { + var args = waiting[name]; + delete waiting[name]; + main.apply(undef, args); + } + return defined[name]; + } + + /** + * Makes a name map, normalizing the name, and using a plugin + * for normalization if necessary. Grabs a ref to plugin + * too, as an optimization. + */ + function makeMap(name, relName) { + var prefix, plugin, + index = name.indexOf('!'); + + if (index !== -1) { + prefix = normalize(name.slice(0, index), relName); + name = name.slice(index + 1); + plugin = callDep(prefix); + + //Normalize according + if (plugin && plugin.normalize) { + name = plugin.normalize(name, makeNormalize(relName)); + } else { + name = normalize(name, relName); + } + } else { + name = normalize(name, relName); + } + + //Using ridiculous property names for space reasons + return { + f: prefix ? prefix + '!' + name : name, //fullName + n: name, + p: plugin + }; + } + + main = function (name, deps, callback, relName) { + var args = [], + usingExports, + cjsModule, depName, i, ret, map; + + //Use name if no relName + if (!relName) { + relName = name; + } + + //Call the callback to define the module, if necessary. + if (typeof callback === 'function') { + + //Default to require, exports, module if no deps if + //the factory arg has any arguments specified. + if (!deps.length && callback.length) { + deps = ['require', 'exports', 'module']; + } + + //Pull out the defined dependencies and pass the ordered + //values to the callback. + for (i = 0; i < deps.length; i++) { + map = makeMap(deps[i], relName); + depName = map.f; + + //Fast path CommonJS standard dependencies. + if (depName === "require") { + args[i] = makeRequire(name); + } else if (depName === "exports") { + //CommonJS module spec 1.1 + args[i] = defined[name] = {}; + usingExports = true; + } else if (depName === "module") { + //CommonJS module spec 1.1 + cjsModule = args[i] = { + id: name, + uri: '', + exports: defined[name] + }; + } else if (defined.hasOwnProperty(depName) || waiting.hasOwnProperty(depName)) { + args[i] = callDep(depName); + } else if (map.p) { + map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); + args[i] = defined[depName]; + } else { + throw name + ' missing ' + depName; + } + } + + ret = callback.apply(defined[name], args); + + if (name) { + //If setting exports via "module" is in play, + //favor that over return value and exports. After that, + //favor a non-undefined return value over exports use. + if (cjsModule && cjsModule.exports !== undef) { + defined[name] = cjsModule.exports; + } else if (!usingExports) { + //Use the return value from the function. + defined[name] = ret; + } + } + } else if (name) { + //May just be an object definition for the module. Only + //worry about defining if have a module name. + defined[name] = callback; + } + }; + + requirejs = req = function (deps, callback, relName, forceSync) { + if (typeof deps === "string") { + + //Just return the module wanted. In this scenario, the + //deps arg is the module name, and second arg (if passed) + //is just the relName. + //Normalize module name, if it contains . or .. + return callDep(makeMap(deps, callback).f); + } else if (!deps.splice) { + //deps is a config object, not an array. + //Drop the config stuff on the ground. + if (callback.splice) { + //callback is an array, which means it is a dependency list. + //Adjust args if there are dependencies + deps = callback; + callback = arguments[2]; + } else { + deps = []; + } + } + + //Simulate async callback; + if (forceSync) { + main(undef, deps, callback, relName); + } else { + setTimeout(function () { + main(undef, deps, callback, relName); + }, 15); + } + + return req; + }; + + /** + * Just drops the config on the floor, but returns req in case + * the config return value is used. + */ + req.config = function () { + return req; + }; + + /** + * Export require as a global, but only if it does not already exist. + */ + if (!require) { + require = req; + } + + define = function (name, deps, callback) { + + //This module may not have dependencies + if (!deps.splice) { + //deps is not an array, so probably means + //an object literal or factory function for + //the value. Adjust args. + callback = deps; + deps = []; + } + + if (define.unordered) { + waiting[name] = [name, deps, callback]; + } else { + main(name, deps, callback); + } + }; + + define.amd = { + jQuery: true + }; +}()); diff --git a/assets/js/libs/backbone.js b/assets/js/libs/backbone.js new file mode 100644 index 0000000..3373c95 --- /dev/null +++ b/assets/js/libs/backbone.js @@ -0,0 +1,1431 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object (`window` in the browser, `global` + // on the server). + var root = this; + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to slice/splice. + var slice = Array.prototype.slice; + var splice = Array.prototype.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both CommonJS and the browser. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '0.9.2'; + + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + + // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. + var $ = root.jQuery || root.Zepto || root.ender; + + // Set the JavaScript library that will be used for DOM manipulation and + // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery, + // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an + // alternate JavaScript library (or a mock library for testing your views + // outside of a browser). + Backbone.setDomLibrary = function(lib) { + $ = lib; + }; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // ----------------- + + // Regular expression used to split event strings + var eventSplitter = /\s+/; + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback functions + // to an event; trigger`-ing an event fires all callbacks in succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind one or more space separated events, `events`, to a `callback` + // function. Passing `"all"` will bind the callback to all events fired. + on: function(events, callback, context) { + + var calls, event, node, tail, list; + if (!callback) return this; + events = events.split(eventSplitter); + calls = this._callbacks || (this._callbacks = {}); + + // Create an immutable callback list, allowing traversal during + // modification. The tail is an empty object that will always be used + // as the next node. + while (event = events.shift()) { + list = calls[event]; + node = list ? list.tail : {}; + node.next = tail = {}; + node.context = context; + node.callback = callback; + calls[event] = {tail: tail, next: list ? list.next : node}; + } + + return this; + }, + + // Remove one or many callbacks. If `context` is null, removes all callbacks + // with that function. If `callback` is null, removes all callbacks for the + // event. If `events` is null, removes all bound callbacks for all events. + off: function(events, callback, context) { + var event, calls, node, tail, cb, ctx; + + // No events, or removing *all* events. + if (!(calls = this._callbacks)) return; + if (!(events || callback || context)) { + delete this._callbacks; + return this; + } + + // Loop through the listed events and contexts, splicing them out of the + // linked list of callbacks if appropriate. + events = events ? events.split(eventSplitter) : _.keys(calls); + while (event = events.shift()) { + node = calls[event]; + delete calls[event]; + if (!node || !(callback || context)) continue; + // Create a new list, omitting the indicated callbacks. + tail = node.tail; + while ((node = node.next) !== tail) { + cb = node.callback; + ctx = node.context; + if ((callback && cb !== callback) || (context && ctx !== context)) { + this.on(event, cb, ctx); + } + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(events) { + var event, node, calls, tail, args, all, rest; + if (!(calls = this._callbacks)) return this; + all = calls.all; + events = events.split(eventSplitter); + rest = slice.call(arguments, 1); + + // For each event, walk through the linked list of callbacks twice, + // first to trigger the event, then to trigger any `"all"` callbacks. + while (event = events.shift()) { + if (node = calls[event]) { + tail = node.tail; + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, rest); + } + } + if (node = all) { + tail = node.tail; + args = [event].concat(rest); + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, args); + } + } + } + + return this; + } + + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Backbone.Model + // -------------- + + // Create a new model, with defined attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + attributes || (attributes = {}); + if (options && options.parse) attributes = this.parse(attributes); + if (defaults = getValue(this, 'defaults')) { + attributes = _.extend({}, defaults, attributes); + } + if (options && options.collection) this.collection = options.collection; + this.attributes = {}; + this._escapedAttributes = {}; + this.cid = _.uniqueId('c'); + this.changed = {}; + this._silent = {}; + this._pending = {}; + this.set(attributes, {silent: true}); + // Reset change tracking. + this.changed = {}; + this._silent = {}; + this._pending = {}; + this._previousAttributes = _.clone(this.attributes); + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // A hash of attributes that have silently changed since the last time + // `change` was called. Will become pending attributes on the next call. + _silent: null, + + // A hash of attributes that have changed since the last `'change'` event + // began. + _pending: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + var html; + if (html = this._escapedAttributes[attr]) return html; + var val = this.get(attr); + return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"` unless + // you choose to silence it. + set: function(key, value, options) { + var attrs, attr, val; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + + // Extract attributes and options. + options || (options = {}); + if (!attrs) return this; + if (attrs instanceof Model) attrs = attrs.attributes; + if (options.unset) for (attr in attrs) attrs[attr] = void 0; + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + var changes = options.changes = {}; + var now = this.attributes; + var escaped = this._escapedAttributes; + var prev = this._previousAttributes || {}; + + // For each `set` attribute... + for (attr in attrs) { + val = attrs[attr]; + + // If the new and current value differ, record the change. + if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) { + delete escaped[attr]; + (options.silent ? this._silent : changes)[attr] = true; + } + + // Update or delete the current value. + options.unset ? delete now[attr] : now[attr] = val; + + // If the new and previous value differ, record the change. If not, + // then remove changes for this attribute. + if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) { + this.changed[attr] = val; + if (!options.silent) this._pending[attr] = true; + } else { + delete this.changed[attr]; + delete this._pending[attr]; + } + } + + // Fire the `"change"` events. + if (!options.silent) this.change(options); + return this; + }, + + // Remove an attribute from the model, firing `"change"` unless you choose + // to silence it. `unset` is a noop if the attribute doesn't exist. + unset: function(attr, options) { + (options || (options = {})).unset = true; + return this.set(attr, null, options); + }, + + // Clear all attributes on the model, firing `"change"` unless you choose + // to silence it. + clear: function(options) { + (options || (options = {})).unset = true; + return this.set(_.clone(this.attributes), options); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overriden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp); + }; + options.error = Backbone.wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, value, options) { + var attrs, current; + + // Handle both `("key", value)` and `({key: value})` -style calls. + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + options = options ? _.clone(options) : {}; + + // If we're "wait"-ing to set changed attributes, validate early. + if (options.wait) { + if (!this._validate(attrs, options)) return false; + current = _.clone(this.attributes); + } + + // Regular saves `set` attributes before persisting to the server. + var silentOptions = _.extend({}, options, {silent: true}); + if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { + return false; + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + var serverAttrs = model.parse(resp, xhr); + if (options.wait) { + delete options.wait; + serverAttrs = _.extend(attrs || {}, serverAttrs); + } + if (!model.set(serverAttrs, options)) return false; + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + + // Finish configuring and sending the Ajax request. + options.error = Backbone.wrapError(options.error, model, options); + var method = this.isNew() ? 'create' : 'update'; + var xhr = (this.sync || Backbone.sync).call(this, method, this, options); + if (options.wait) this.set(current, silentOptions); + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var triggerDestroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + if (this.isNew()) { + triggerDestroy(); + return false; + } + + options.success = function(resp) { + if (options.wait) triggerDestroy(); + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + + options.error = Backbone.wrapError(options.error, model, options); + var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); + if (!options.wait) triggerDestroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, xhr) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Call this method to manually fire a `"change"` event for this model and + // a `"change:attribute"` event for each changed attribute. + // Calling this will cause all objects observing the model to update. + change: function(options) { + options || (options = {}); + var changing = this._changing; + this._changing = true; + + // Silent changes become pending changes. + for (var attr in this._silent) this._pending[attr] = true; + + // Silent changes are triggered. + var changes = _.extend({}, options.changes, this._silent); + this._silent = {}; + for (var attr in changes) { + this.trigger('change:' + attr, this, this.get(attr), options); + } + if (changing) return this; + + // Continue firing `"change"` events while there are pending changes. + while (!_.isEmpty(this._pending)) { + this._pending = {}; + this.trigger('change', this, options); + // Pending and silent changes still remain. + for (var attr in this.changed) { + if (this._pending[attr] || this._silent[attr]) continue; + delete this.changed[attr]; + } + this._previousAttributes = _.clone(this.attributes); + } + + this._changing = false; + return this; + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (!arguments.length) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false, old = this._previousAttributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (!arguments.length || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Check if the model is currently in a valid state. It's only possible to + // get into an *invalid* state if you're using silent changes. + isValid: function() { + return !this.validate(this.attributes); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. If a specific `error` callback has + // been passed, call that instead of firing the general `"error"` event. + _validate: function(attrs, options) { + if (options.silent || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validate(attrs, options); + if (!error) return true; + if (options && options.error) { + options.error(this, error, options); + } else { + this.trigger('error', this, error, options); + } + return false; + } + + }); + + // Backbone.Collection + // ------------------- + + // Provides a standard collection class for our sets of models, ordered + // or unordered. If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, {silent: true, parse: options.parse}); + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Add a model, or list of models to the set. Pass **silent** to avoid + // firing the `add` event for every new model. + add: function(models, options) { + var i, index, length, model, cid, id, cids = {}, ids = {}, dups = []; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + + // Begin by turning bare objects into model references, and preventing + // invalid models or duplicate models from being added. + for (i = 0, length = models.length; i < length; i++) { + if (!(model = models[i] = this._prepareModel(models[i], options))) { + throw new Error("Can't add an invalid model to a collection"); + } + cid = model.cid; + id = model.id; + if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) { + dups.push(i); + continue; + } + cids[cid] = ids[id] = model; + } + + // Remove duplicates. + i = dups.length; + while (i--) { + models.splice(dups[i], 1); + } + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + for (i = 0, length = models.length; i < length; i++) { + (model = models[i]).on('all', this._onModelEvent, this); + this._byCid[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + + // Insert models into the collection, re-sorting if needed, and triggering + // `add` events unless silenced. + this.length += length; + index = options.at != null ? options.at : this.models.length; + splice.apply(this.models, [index, 0].concat(models)); + if (this.comparator) this.sort({silent: true}); + if (options.silent) return this; + for (i = 0, length = this.models.length; i < length; i++) { + if (!cids[(model = this.models[i]).cid]) continue; + options.index = i; + model.trigger('add', model, this, options); + } + return this; + }, + + // Remove a model, or a list of models from the set. Pass silent to avoid + // firing the `remove` event for every model removed. + remove: function(models, options) { + var i, l, index, model; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + for (i = 0, l = models.length; i < l; i++) { + model = this.getByCid(models[i]) || this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byCid[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return this; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, options); + return model; + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: 0}, options)); + return model; + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Get a model from the set by id. + get: function(id) { + if (id == null) return void 0; + return this._byId[id.id != null ? id.id : id]; + }, + + // Get a model from the set by client id. + getByCid: function(cid) { + return cid && this._byCid[cid.cid || cid]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of `filter`. + where: function(attrs) { + if (_.isEmpty(attrs)) return []; + return this.filter(function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + options || (options = {}); + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + var boundComparator = _.bind(this.comparator, this); + if (this.comparator.length == 1) { + this.models = this.sortBy(boundComparator); + } else { + this.models.sort(boundComparator); + } + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.map(this.models, function(model){ return model.get(attr); }); + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any `add` or `remove` events. Fires `reset` when finished. + reset: function(models, options) { + models || (models = []); + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + this._reset(); + this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `add: true` is passed, appends the + // models to the collection instead of resetting. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === undefined) options.parse = true; + var collection = this; + var success = options.success; + options.success = function(resp, status, xhr) { + collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); + if (success) success(collection, resp); + }; + options.error = Backbone.wrapError(options.error, collection, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + var coll = this; + options = options ? _.clone(options) : {}; + model = this._prepareModel(model, options); + if (!model) return false; + if (!options.wait) coll.add(model, options); + var success = options.success; + options.success = function(nextModel, resp, xhr) { + if (options.wait) coll.add(nextModel, options); + if (success) { + success(nextModel, resp); + } else { + nextModel.trigger('sync', model, resp, options); + } + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, xhr) { + return resp; + }, + + // Proxy to _'s chain. Can't be proxied the same way the rest of the + // underscore methods are proxied because it relies on the underscore + // constructor. + chain: function () { + return _(this.models).chain(); + }, + + // Reset all internal state. Called when the collection is reset. + _reset: function(options) { + this.length = 0; + this.models = []; + this._byId = {}; + this._byCid = {}; + }, + + // Prepare a model or hash of attributes to be added to this collection. + _prepareModel: function(model, options) { + options || (options = {}); + if (!(model instanceof Model)) { + var attrs = model; + options.collection = this; + model = new this.model(attrs, options); + if (!model._validate(model.attributes, options)) model = false; + } else if (!model.collection) { + model.collection = this; + } + return model; + }, + + // Internal method to remove a model's ties to a collection. + _removeReference: function(model) { + if (this == model.collection) { + delete model.collection; + } + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event == 'add' || event == 'remove') && collection != this) return; + if (event == 'destroy') { + this.remove(model, options); + } + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', + 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', + 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', + 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', + 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + return _[method].apply(_, [this.models].concat(_.toArray(arguments))); + }; + }); + + // Backbone.Router + // ------------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var namedParam = /:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + Backbone.history || (Backbone.history = new History); + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (!callback) callback = this[name]; + Backbone.history.route(route, _.bind(function(fragment) { + var args = this._extractParameters(route, fragment); + callback && callback.apply(this, args); + this.trigger.apply(this, ['route:' + name].concat(args)); + Backbone.history.trigger('route', this, name, args); + }, this)); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + var routes = []; + for (var route in this.routes) { + routes.unshift([route, this.routes[route]]); + } + for (var i = 0, l = routes.length; i < l; i++) { + this.route(routes[i][0], routes[i][1], this[routes[i][1]]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(namedParam, '([^\/]+)') + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted parameters. + _extractParameters: function(route, fragment) { + return route.exec(fragment).slice(1); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on URL fragments. If the + // browser does not support `onhashchange`, falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + }; + + // Cached regex for cleaning leading hashes and slashes . + var routeStripper = /^[#\/]/; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(windowOverride) { + var loc = windowOverride ? windowOverride.location : window.location; + var match = loc.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) fragment += search; + } else { + fragment = this.getHash(); + } + } + if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length); + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + if (oldIE) { + this.iframe = $('