Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 607b83dbb3ddc50f90118339b1ccb7949eadee03 @robdodson committed May 29, 2012
4 .npmignore
@@ -0,0 +1,4 @@
+dist/
+npm-debug.log
+.lock-wscript
+build/node_modules/grunt/node_modules/glob/build
33 app/config.js
@@ -0,0 +1,33 @@
+// 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",
+ mocha: "../test/mocha/mocha",
+ chai: "../test/chai/chai",
+ sinon: "../test/sinon/sinon",
+
+ // Shim Plugin
+ use: "../assets/js/plugins/use"
+ },
+
+ use: {
+ backbone: {
+ deps: ["use!underscore", "jquery"],
+ attach: "Backbone"
+ },
+
+ underscore: {
+ attach: "_"
+ }
+ }
+});
81 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);
+ }
+ });
+
+});
50 app/modules/example.js
@@ -0,0 +1,50 @@
+define([
+ "namespace",
+
+ // Libs
+ "use!backbone"
+
+ // Modules
+
+ // Plugins
+],
+
+function(namespace, Backbone) {
+
+ // Create a new module
+ var Example = namespace.module();
+
+ // Example extendings
+ Example.Photo = Backbone.Model.extend({
+ defaults: {
+ src: '/images/placeholder.jpg',
+ caption: 'Waiting for content...'
+ }
+ });
+
+ 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;
+
+});
65 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)
+ };
+});
355 app/templates/example.html
@@ -0,0 +1,355 @@
+<header>
+ <img src="/assets/img/backbone.png">
+ <hr>
+
+ <div>
+ <h2 class="congrats">Congratulations!</h2>
+ <h2>Seeing this means you have installed Backbone Boilerplate correctly.</h2>
+
+ <p>Now that you have the easiest and most-powerful Backbone boilerplate available,
+ you're probably wondering how to use it to start building applications...</p>
+ </div>
+</header>
+
+<section id="toc">
+ <h1>Contents</h1>
+
+ <ul>
+ <li><a data-bypass href="#overview">Overview</a>
+ <li><a data-bypass href="#getting-help">Getting help</a>
+ <li><a data-bypass href="#writing-app">Writing your application</a>
+ <ul class="nested">
+ <li><a data-bypass href="#cleaning">Cleaning out default files and code</a>
+ <ul class="nested">
+ <li><a data-bypass href="#git-history">Removing the Git history</a>
+ <li><a data-bypass href="#test-dir">Removing the test directory</a>
+ <li><a data-bypass href="#build-process">Removing the build process</a>
+ <li><a data-bypass href="#favicon">Changing the Favicon</a>
+ <li><a data-bypass href="#app-code">Removing default application code</a>
+ <li><a data-bypass href="#default-routes">Removing the default routes</a>
+ <li><a data-bypass href="#default-assets">Removing default assets</a>
+ </ul>
+ <li><a data-bypass href="#namespace">Namespace</a>
+ <li><a data-bypass href="#modules">Creating a module</a>
+ <li><a data-bypass href="#templates">Working with templates</a>
+ <li><a data-bypass href="#events">Working with Application Wide Events</a>
+ <li><a data-bypass href="#plugins">Working with libraries and plugins</a>
+ </ul>
+ <li><a data-bypass href="#custom-build">Using the build tool</a>
+ <ul class="nested">
+ <li><a data-bypass href="#running">Running with the defaults</a>
+ <li><a data-bypass href="#customizing">Customizing the build configuration</a>
+ <li><a data-bypass href="#server">Using the development server</a>
+ <li><a data-bypass href="#adding-tasks">Adding new tasks</a>
+ </ul>
+ <li><a data-bypass href="#useful-resources">Useful Resources</a>
+ </ul>
+</section>
+
+<section>
+ <h2 id="overview">Overview</h2>
+
+ <p>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.</p>
+
+ <h3 id="core-features">Core Features</h3>
+ <ul>
+ <li><a target="_blank" href="https://github.com/h5bp/html5-boilerplate">HTML5 Boilerplate</a> included.
+ <li>Managed filesystem structure for application code, assets, tests, and distribution.
+ <li>Snippets to make common tasks easier: modules, HTML5 History API/Hash navigation, template loading and application events.
+ <li>Flexible and extendable build system.
+ <ul class="nested">
+ <li>Concatenate and minify all your libraries, application code, templates and CSS down to reduce transmission time.
+ <li>Compile underscore templates to prevent pre-processing on the client.
+ </ul>
+ </ul>
+</section>
+
+<section>
+ <h2 id="getting-help">Getting help</h2>
+
+ <p>If you're encountering issues, need assistance, or have a question that hasn't been answered in this
+ tutorial or <a target="blank" href="https://github.com/tbranyen/backbone-boilerplate">the GitHub project page</a>
+ you may find help in one of these places:</p>
+
+ <ul>
+ <li>IRC - #backbone-boilerplate on irc.freenode.net
+ <li><a target="blank" href="http://github.com/tbranyen/backbone-boilerplate/issues">GitHub Issues</a> - Please report if you've found an issue,
+ bug, or controversial request.
+ </ul>
+
+ <p>I want this project to be the best it possibly can and represent the interests of the community, <b>please</b>
+ submit issues with features you find useful and anything that you question.</p>
+</section>
+
+<section>
+ <h2 id="writing-app">Writing your application</h2>
+ <p>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.
+ </p>
+
+ <p class="clues"><b>Strongly recommend you read through this tutorial before cleaning out any files that may hold clues on how to use the Boilerplate.</b></p>
+
+ <h3 id="cleaning">Cleaning out default files and code</h3>
+ <p>There are several places where customization may be required.</p>
+
+ <ul>
+ <li><h4 id="git-history">Removing the Git history</h4>
+ <p>If you cloned the Backbone Boilerplate with Git, you should delete the git directory and then initialize your own Git history:
+
+ <pre><code>
+ $ rm -rf .git
+ $ git init
+ </code></pre>
+ </p>
+
+ <li><h4 id="test-dir">Removing the test directory</h4>
+ <p>If you are not planning on testing your application with QUnit you should delete this directory.</p>
+
+ <li><h4 id="build-process">Removing the build process</h4>
+ <p>If you are not planning on using the build tool, delete the
+ <code>grunt.js</code> file. It contains configuration you will not need.
+ </p>
+
+ <li><h4 id="favicon">Changing the Favicon</h4>
+ <p>At the root level of the project simply change the <code>favicon.ico</code> file to point to your own branded icon.</p>
+
+ <li><h4 id="app-code">Removing default application code</h4>
+ <p>This tutorial is rendered in the <code>app/modules/example.js</code> file and written in <code>app/templates/example.html</code>.
+ Both of these files are safe to remove.</p>
+
+ <li><h4 id="default-routes">Removing the default routes</h4>
+ <p>Routes are defined in the <code>app/main.js</code> file. Familiarize yourself with it's contents. You'll notice the default router has two existing routes and callback defined, reset it to:
+
+ <pre><code>
+ // Defining the application router, you can attach sub routers here.
+ var Router = Backbone.Router.extend({
+ routes: {
+ "": "index",
+ },
+
+ index: function() {
+ // Put your homepage route logic here
+ }
+ });
+ </code></pre>
+ </p>
+
+
+ <p>Above the Router definition you'll see a reference to the example module,
+ this is safe to delete as well.
+
+ <pre><code>
+ // Include the example module
+ var Example = namespace.module("example");
+ </code></pre>
+ </p>
+
+ <li><h4 id="default-assets">Removing default assets</h4>
+ <p>The default styles for this tutorial are stored in <code>assets/css/style.css</code>. You will probably want to remove these since they only make sense for this specific page. They start on <code>Line 209</code>. With the following H5BP header:
+
+ <pre><code>
+ /* ==|== primary styles =====================================================
+ Author: Backbone Boilerplate <Change to your Name>
+ ========================================================================== */
+ </code></pre>
+ </p>
+
+ <p>You may also want to change the name to yours, if you're planning on putting your custom CSS here as well.</p>
+
+ <p>You should delete the <code>assets/img/backbone.png</code> file if you are not planning on using it in your app.</p>
+ </ul>
+
+ <h3 id="namespace">Namespace</h3>
+ <p>The <code>namespace.js</code> file is very important since it contains logic that should
+ exist for every module in your application. This also contains the <code>module</code>
+ shortcut function and <code>fetchTemplate</code> function.</p>
+ </p>
+
+ <h3 id="modules">Creating a module</h3>
+ <p>Following the Bocoup post on <a data-bypass target="_blank" href="http://weblog.bocoup.com/organizing-your-backbone-js-application-with-modules">Organizing Your Backbone.js Application With Modules</a> this boilerplate provides the same module definition structure.
+
+ Modules are placed in the <code>app/modules/</code> directory. There is an example module
+ there named: <code>example.js</code>. The actual module definition function is located
+ inside the <code>app/index.js</code> file. You create and reference modules with the same
+ function call: <code>namespace.module(&quot;&lt;module_name&gt;&quot;)</code>.
+
+ 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:
+
+ <pre><code>
+ MyModule.Views.Detailed = Backbone.View.extend({ /* ... */ });
+
+ MyModule.Views.Main = Backbone.View.extend({ /* ... */ });
+ </code></pre>
+ </p>
+
+ <p>
+ Attaching Models/Collections/Routers happen on the same level of the module,
+ like so:
+
+ <pre><code>
+ MyModule.Model = Backbone.Model.extend({ /* ... */ });
+
+ MyModule.Router = Backbone.Router.extend({ /* ... */ });
+ </code></pre>
+ </p>
+
+ <h3 id="templates">Working with templates</h3>
+ <p>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.</p>
+
+ <p>This tutorial itself is a template that exists in <code>app/templates/example.html</code>. 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:
+
+ <pre><code>
+ namespace.fetchTemplate("app/templates/name.html", function(template) {
+ // Template here is a function, that accepts an object. Identical to _.template.
+ console.log(template({ ... }));
+ });
+ </code></pre>
+ </p>
+
+ <p>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 <code>app/modules/example.js</code> module.</p>
+
+ <p>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.</p>
+
+ <p>You can access a compiled template like so:
+
+ <pre><code>
+ var template = window.JST["app/modules/example.html"];
+ template({ ... });
+ </code></pre>
+ </p>
+
+ <h3 id="events">Working with Application Wide Events</h3>
+ <p>Application wide events provide a convenient way for modules to communicate with each other. <code>namespace.app</code> references a copy of the Backbone.Events object. <a href="http://documentcloud.github.com/backbone/#Events">More information on Backbone Events</a></p>
+
+ <p>
+ To bind a callback function to an event:
+
+ <pre><code>
+ //binding an annonymous function to the event "all" with a context of <code>this</code>.
+ namespace.app.on("all", function(){...}, this);
+ </code></pre>
+ </p>
+
+ <p>
+ To remove a callback function (or many) from an event:
+
+ <pre><code>
+ // 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();
+ </code></pre>
+ </p>
+
+ <p>
+ To trigger the "change" event:
+
+ <pre><code>
+ namespace.app.trigger("change", [*args]);
+ </code></pre>
+ </p>
+
+ <h3 id="plugins">Working with libraries and plugins</h3>
+ <p>Libraries and plugins are easily added to the application, by placing them inside the <code>assets/js/libs/</code> directory.
+ If you have many plugins in your application, it may make sense to create a separate folder such as <code>assets/js/plugins/</code>
+ for them.</p>
+</section>
+
+<section>
+ <h2 id="custom-build">Using the build tool</h2>
+ <p>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*.</p>
+
+ <h3 id="running">Running with the defaults</h3>
+ <p>To run the defaults, execute the following command from the project root,
+ and *not from inside the build folder*.
+
+ <pre><code>
+ node build
+ </code></pre>
+ </p>
+
+ <p>
+ 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 <code>dist/release</code> folder.</p>
+
+ <h3 id="customizing">Customizing the build configuration</h3>
+ <p>To customize and configure the build tool, open `build/config.js` and tweak
+the settings.</p>
+
+ <h3 id="server">Using the development server</h3>
+ <p>
+ While writing an application that leverages <code>pushState</code> you can run the
+ following command to run a server that will always resolve to the <code>index.html</code>
+
+ <pre><code>
+ node build/server
+ </code></pre>
+ </p>
+
+ <p>
+ This will spawn up an HTTP server on port <code>8000</code>. 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.</p>
+
+ <h4>Serving the built assets</h4>
+
+ <p>If you are using the build tool in conjunction with this development server
+ you can optionally update the <code>index.html</code> file to remove the existing script
+ tags and uncomment out the scripts tag at the bottom to load the <code>dist/debug</code>
+ or <code>dist/release</code> assets. You can achieve this by specifying either <b>debug</b>
+ or <b>release</b> after the server command, like so:
+
+ <pre><code>
+ node build/server release
+ </code></pre>
+ </p>
+
+ <h3 id="adding-tasks">Adding new tasks</h3>
+ <p>To add a new task into the build system, you simply copy and paste the task JavaScript folder/file into the <code>build/tasks</code> 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 <code>build/config.js</code>
+ file. The last line should look something like:
+
+ <pre><code>
+ task.registerTask("default", "clean lint:files concat jst min mincss new_module_here");
+ </code></pre>
+ </p>
+
+ It's possible the custom task will have additional setup instructions, so make
+ sure you read the README for any task.</p>
+</section>
+
+<section id="useful-resources">
+ <h2>Useful resources</h2>
+
+ <ul>
+ <li><a target="blank" href="http://backbonejs.org/">Backbone documentation</a> - Framework on which Backbone Boilerplate is built.
+ <li><a target="blank" href="http://documentcloud.github.com/underscore/docs/underscore.html">Underscore documentation</a> - Required dependency for Backbone.
+ </ul>
+</section>
1 assets/css/index.css
@@ -0,0 +1 @@
+@import "style.css";
342 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 <Change to your Name>
+ ========================================================================== */
+
+#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; }
+}
BIN assets/img/backbone.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
277 assets/js/libs/almond.js
@@ -0,0 +1,277 @@
+/**
+ * almond 0.0.3 Copyright (c) 2011, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/almond for details
+ */
+/*jslint strict: false, plusplus: false */
+/*global setTimeout: false */
+
+var requirejs, require, define;
+(function (undef) {
+
+ var defined = {},
+ waiting = {},
+ aps = [].slice,
+ main, req;
+
+ if (typeof define === "function") {
+ //If a define is already in play via another AMD loader,
+ //do not overwrite.
+ return;
+ }
+
+ /**
+ * Given a relative module name, like ./something, normalize it to
+ * a real name that can be mapped to a path.
+ * @param {String} name the relative name
+ * @param {String} baseName a real name that the name arg is relative
+ * to.
+ * @returns {String} normalized name
+ */
+ function normalize(name, baseName) {
+ //Adjust any relative paths.
+ if (name && name.charAt(0) === ".") {
+ //If have a base name, try to normalize against it,
+ //otherwise, assume it is a top-level require that will
+ //be relative to baseUrl in the end.
+ if (baseName) {
+ //Convert baseName to array, and lop off the last part,
+ //so that . matches that "directory" and not name of the baseName's
+ //module. For instance, baseName of "one/two/three", maps to
+ //"one/two/three.js", but we want the directory, "one/two" for
+ //this normalization.
+ baseName = baseName.split("/");
+ baseName = baseName.slice(0, baseName.length - 1);
+
+ name = baseName.concat(name.split("/"));
+
+ //start trimDots
+ var i, part;
+ for (i = 0; (part = name[i]); i++) {
+ if (part === ".") {
+ name.splice(i, 1);
+ i -= 1;
+ } else if (part === "..") {
+ if (i === 1 && (name[2] === '..' || name[0] === '..')) {
+ //End of the line. Keep at least one non-dot
+ //path segment at the front so it can be mapped
+ //correctly to disk. Otherwise, there is likely
+ //no path mapping for a path starting with '..'.
+ //This can still fail, but catches the most reasonable
+ //uses of ..
+ break;
+ } else if (i > 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
+ };
+}());
1,431 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 = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
+ this.navigate(fragment);
+ }
+
+ // Depending on whether we're using pushState or hashes, and whether
+ // 'onhashchange' is supported, determine how we check the URL state.
+ if (this._hasPushState) {
+ $(window).bind('popstate', this.checkUrl);
+ } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
+ $(window).bind('hashchange', this.checkUrl);
+ } else if (this._wantsHashChange) {
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+ }
+
+ // Determine if we need to change the base url, for a pushState link
+ // opened by a non-pushState browser.
+ this.fragment = fragment;
+ var loc = window.location;
+ var atRoot = loc.pathname == this.options.root;
+
+ // If we've started off with a route from a `pushState`-enabled browser,
+ // but we're currently in a browser that doesn't support it...
+ if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
+ this.fragment = this.getFragment(null, true);
+ window.location.replace(this.options.root + '#' + this.fragment);
+ // Return immediately as browser will do redirect to new url
+ return true;
+
+ // Or if we've started out with a hash-based route, but we're currently
+ // in a browser where it could be `pushState`-based instead...
+ } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
+ this.fragment = this.getHash().replace(routeStripper, '');
+ window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
+ }
+
+ if (!this.options.silent) {
+ return this.loadUrl();
+ }
+ },
+
+ // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
+ // but possibly useful for unit testing Routers.
+ stop: function() {
+ $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
+ clearInterval(this._checkUrlInterval);
+ History.started = false;
+ },
+
+ // Add a route to be tested when the fragment changes. Routes added later
+ // may override previous routes.
+ route: function(route, callback) {
+ this.handlers.unshift({route: route, callback: callback});
+ },
+
+ // Checks the current URL to see if it has changed, and if it has,
+ // calls `loadUrl`, normalizing across the hidden iframe.
+ checkUrl: function(e) {
+ var current = this.getFragment();
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
+ if (current == this.fragment) return false;
+ if (this.iframe) this.navigate(current);
+ this.loadUrl() || this.loadUrl(this.getHash());
+ },
+
+ // Attempt to load the current URL fragment. If a route succeeds with a
+ // match, returns `true`. If no defined routes matches the fragment,
+ // returns `false`.
+ loadUrl: function(fragmentOverride) {
+ var fragment = this.fragment = this.getFragment(fragmentOverride);
+ var matched = _.any(this.handlers, function(handler) {
+ if (handler.route.test(fragment)) {
+ handler.callback(fragment);
+ return true;
+ }
+ });
+ return matched;
+ },
+
+ // Save a fragment into the hash history, or replace the URL state if the