commit 046d44480241e71cafd252c8cb5168bd29ff92c7 Author: Buddy Sandidge Date: Sat Feb 7 22:40:45 2015 -0800 Initial commit with build step diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..6866ac2 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "vendor" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..219e74d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +scripts/* +node_modules/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3d3d60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +npm-debug.log +static/ diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..598069d --- /dev/null +++ b/.jscsrc @@ -0,0 +1,7 @@ +{ + "preset": "crockford", + "validateIndentation": 2, + "disallowMultipleVarDecl": true, + "disallowDanglingUnderscores": null, + "requireMultipleVarDecl": null +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..708e2a6 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,45 @@ +{ + "bitwise": true, + "camelcase": true, + "curly": true, + "enforceall": true, + "eqeqeq": true, + "es3": true, + "forin": true, + "freeze": true, + "immed": true, + "latedef": true, + "newcap": true, + "noarg": true, + "nocomma": false, + "noempty": true, + "nonbsp": true, + "nonew": true, + "notypeof": true, + "quotmark": true, + "singleGroups": true, + "undef": true, + "unused": true, + + "eqnull": true, + + "maxcomplexity": 4, + "maxdepth": 4, + "maxerr": 50, + "maxlen": 80, + "maxparams": 4, + + "indent": 2, + "globals": { + "define": true, + "describe": true, + "expect": true, + "it": true, + "jasmine": true, + "module": true, + "require": true, + "requirejs": true, + "xdescribe": true, + "xit": true + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..50aacbb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +FROM ubuntu:14.04 +MAINTAINER Buddy Sandidge + +# UTF-8 locale +RUN locale-gen en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe multiverse" \ + > /etc/apt/sources.list.d/multiverse.list +RUN apt-get update && \ + apt-get upgrade -y && \ + yes | apt-get install -y \ + build-essential \ + curl \ + libfontconfig \ + python \ + ruby1.9.1-dev \ + # For PhantomJS 2.0 + g++ flex bison gperf ruby perl libsqlite3-dev libfontconfig1-dev \ + libicu-dev libfreetype6 libssl-dev \ + libpng-dev libjpeg-dev unzip ttf-mscorefonts-installer && \ + rm -r /var/lib/apt/lists + +# PhantomJS 2.0 +# The linux binary is not yet available for download +# We need to build from source +RUN curl --silent --location --output /opt/phantomjs-2.0.0-source.zip \ + https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.0.0-source.zip && \ + unzip -q /opt/phantomjs-2.0.0-source.zip -d /opt && \ + cd /opt/phantomjs-2.0.0 && ./build.sh --confirm && \ + mv /opt/phantomjs-2.0.0/bin/phantomjs /usr/local/bin/phantomjs-2.0.0 && \ + rm -r /opt/phantomjs-2.0.0/ && \ + rm /opt/phantomjs-2.0.0-source.zip + +# PhantomJS 1.9.8 +RUN curl --location --silent --output /opt/phantom-1.9.8.tar.bz2 \ + https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.8-linux-x86_64.tar.bz2 && \ + mkdir /opt/phantomjs && \ + tar xf /opt/phantom-1.9.8.tar.bz2 --strip-components 1 --directory /opt/phantomjs && \ + mv /opt/phantomjs/bin/phantomjs /usr/local/bin/phantomjs-1.9.8 && \ + rm -rf /opt/phantom-1.9.8.tar.bz2 /opt/phantomjs + +# Compass +RUN gem install compass --version 1.0.1 + +# Node +ENV NODE_VERSION 0.10.35 +RUN mkdir -p /opt/node && \ + curl --silent --output /opt/node.tar.gz \ + https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz && \ + tar xzf /opt/node.tar.gz --strip-components 1 --directory /opt/node && \ + rm /opt/node.tar.gz + +RUN cd /usr/local/bin/ && ln -s phantomjs-1.9.8 phantomjs + +ENV PATH /opt/node/bin:/opt/phantom-talk/node_modules/.bin:$PATH +ENV PHANTOMJS_BIN /usr/local/bin/phantomjs + +# Mount this in the docker container +VOLUME ["/opt/phantom-talk"] +WORKDIR /opt/phantom-talk +COPY package.json /opt/phantom-talk/package.json +RUN ["npm", "install"] +EXPOSE 9900 +ENTRYPOINT ["npm"] diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..596a27a --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,93 @@ +var _ = require('lodash'); +var requireConfig = require('./assets/app/config'); + +function buildRequireConfig(options) { + 'use strict'; + if (options == null) { + options = {}; + } + var ret = {}; + _.extend(ret, requireConfig('../..'), options, { + baseUrl: 'static/app', + name: 'main', + optimize: 'none', + out: './static/app.js' + }); + return ret; +} + +module.exports = function (grunt) { + 'use strict'; + + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-compass'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-requirejs'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-jscs'); + grunt.loadNpmTasks('grunt-karma'); + + grunt.initConfig({ + requirejs: { + build: { + options: buildRequireConfig({ + generateSourceMaps: true, + optimize: 'none' + }) + } + }, + + copy: { + app: { + src: '**/*.js', + dest: '<%= clean.app %>', + cwd: './assets/app', + expand: true + } + }, + + clean: { + app: './static/app' + }, + + compass: { + dist: { + options: { + sassDir: 'assets/style', + cssDir: 'static/style', + importPath: ['vendor/normalize-scss'] + } + } + }, + + karma: {unit: {configFile: 'config/test.js'}}, + + jshint: { + options: {jshintrc: './.jshintrc'}, + grunt: { + files: {src: 'Gruntfile.js'} + }, + test: { + files: {src: './test/**/*.js'} + } + }, + + jscs: { + src: ['Gruntfile.js', 'test/**/*.js'], + options: {config: '.jscsrc'} + } + + }); + + grunt.registerTask('test', ['jshint', 'jscs', 'karma']); + grunt.registerTask('dev', ['default']); + + grunt.registerTask('default', [ + 'clean', + 'compass', + 'copy', + 'requirejs', + 'jshint' + ]); +}; diff --git a/assets/app/app.js b/assets/app/app.js new file mode 100644 index 0000000..26e9ade --- /dev/null +++ b/assets/app/app.js @@ -0,0 +1,22 @@ +define(['marionette', 'jquery'], function AppDefine(Marionette, $) { + 'use strict'; + + var Application = Marionette.Application; + + function App() { + Application.apply(this, arguments); + } + App.prototype = new Application(); + + App.prototype.onStart = function onStart(options) { + if (options == null) { + options = {}; + } + if (options.el == null) { + options.el = 'body'; + } + $(options.el).text('started'); + }; + + return App; +}); diff --git a/assets/app/config.js b/assets/app/config.js new file mode 100644 index 0000000..d0fc346 --- /dev/null +++ b/assets/app/config.js @@ -0,0 +1,28 @@ +(function (root, factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + define('config', [], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.RequireConfig = factory(); + } +}(this, function () { + 'use strict'; + + return function (prefix) { + prefix = prefix || ''; + + return { + paths: { + backbone: prefix + '/vendor/backbone/backbone', + jquery: prefix + '/vendor/jquery/dist/jquery', + marionette: prefix + '/vendor/marionette/lib/backbone.marionette', + underscore: prefix + '/vendor/underscore/underscore' + }, + shim: { + underscore: {exports: '_'} + } + }; + }; +})); diff --git a/assets/app/main.js b/assets/app/main.js new file mode 100644 index 0000000..30d81cc --- /dev/null +++ b/assets/app/main.js @@ -0,0 +1,9 @@ +define('main', ['config', 'app'], function main(config, App) { + 'use strict'; + requirejs.config(config()); + return function main(options) { + var app = new App(); + app.start(options); + return app; + }; +}); diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..87d4d43 --- /dev/null +++ b/assets/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + +
+ + + + + diff --git a/assets/style/main.scss b/assets/style/main.scss new file mode 100644 index 0000000..421d7de --- /dev/null +++ b/assets/style/main.scss @@ -0,0 +1,4 @@ +@import "_normalize"; +body { + background-color: #bada55; +} diff --git a/bin/deck b/bin/deck new file mode 100755 index 0000000..ecc985a --- /dev/null +++ b/bin/deck @@ -0,0 +1,30 @@ +#!/usr/bin/env node +//# vi: ft=javascript + +var fs = require('fs'); + +var handlebars = require('handlebars'); +var express = require('express'); + +var app = express(); +var staticHandler = express['static'].bind(express); + +app.get('/', function index(req, res) { + var indexFile = __dirname + '/../assets/index.html'; + + function compileServe(err, text) { + if (err) { + res.writeHead(500, {'Content-Type': 'text/plain'}, err); + return; + } + var tmpl = handlebars.compile(text); + res.send(tmpl()); + } + + fs.readFile(indexFile, {encoding: 'utf8'}, compileServe); +}); + +app.use('/vendor', staticHandler('vendor')); +app.use('/static', staticHandler('static')); + +app.listen(9900); diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..05fadb2 --- /dev/null +++ b/bower.json @@ -0,0 +1,24 @@ +{ + "name": "phantom-talk", + "version": "0.0.0", + "authors": [ + "Buddy Sandidge " + ], + "description": "PhantomJS Talk for JS.LA", + "main": "bin/deck.js", + "license": "MIT", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "vendor", + "test", + "tests" + ], + "dependencies": { + "marionette": "~2.3.1", + "normalize-scss": "~3.0.2", + "requirejs": "~2.1.15" + } +} diff --git a/config/test.js b/config/test.js new file mode 100644 index 0000000..49f2882 --- /dev/null +++ b/config/test.js @@ -0,0 +1,59 @@ +// Karma configuration + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '..', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine', 'requirejs'], + + // list of files / patterns to load in the browser + files: [ + {pattern: 'vendor/**/*.js', included: false}, + {pattern: 'test/**/!(test-main).js', included: false}, + {pattern: 'static/**/!(config).js', included: false}, + {pattern: 'static/**/*.map', included: false}, + {pattern: 'static/app/config.js', included: true}, + {pattern: 'test/test-main.js', included: true} + ], + + // list of files to exclude + exclude: ['**/*.swp'], + + // preprocess matching files before serving them to the browser + // available preprocessors: + // https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: {}, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + // web server port + port: 9900, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR + // || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests + // whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: + // https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..8691c64 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "phantom-talk", + "version": "0.0.1", + "description": "Slide deck for PhantomJS talk", + "main": "lib/deck.js", + "scripts": { + "dev": "grunt dev", + "deck": "./bin/deck", + "test": "grunt test" + }, + "author": "Buddy Sandidge", + "license": "ISC", + "devDependencies": { + "bower": "^1.3.12", + "grunt": "^0.4.5", + "grunt-cli": "^0.1.13", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-compass": "^1.0.1", + "grunt-contrib-copy": "^0.7.0", + "grunt-contrib-handlebars": "^0.9.2", + "grunt-contrib-jshint": "^0.11.0", + "grunt-contrib-requirejs": "^0.4.4", + "grunt-contrib-watch": "^0.6.1", + "grunt-jscs": "^1.2.0", + "grunt-karma": "^0.10.1", + "jasmine-core": "^2.2.0", + "karma": "^0.12.31", + "karma-jasmine": "^0.3.5", + "karma-phantomjs-launcher": "^0.1.4", + "karma-requirejs": "^0.2.2", + "lodash": "^3.1.0", + "requirejs": "^2.1.15" + }, + "dependencies": { + "express": "^4.11.2", + "handlebars": "^2.0.0" + } +} diff --git a/scripts/bash b/scripts/bash new file mode 100755 index 0000000..5f68dd9 --- /dev/null +++ b/scripts/bash @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +dir=$(dirname $0) +source $dir/common + +docker run \ + --interactive \ + --tty \ + --volume $(abspath $dir/..):/opt/phantom-talk \ + --publish=9876:9876 \ + --entrypoint /bin/bash \ + phantom-talk diff --git a/scripts/build b/scripts/build new file mode 100755 index 0000000..ccf22cd --- /dev/null +++ b/scripts/build @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +docker build --tag phantom-talk $(dirname $0)/.. diff --git a/scripts/common b/scripts/common new file mode 100644 index 0000000..9c5a1e8 --- /dev/null +++ b/scripts/common @@ -0,0 +1,3 @@ +function abspath { + python -c 'import sys, os; print(os.path.abspath(sys.argv[1]))' "$1" +} diff --git a/scripts/dev b/scripts/dev new file mode 100755 index 0000000..dd3c3d1 --- /dev/null +++ b/scripts/dev @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +dir=$(dirname $0) +source $dir/common + +docker run \ + --volume $(abspath $dir/..):/opt/phantom-talk \ + --publish 9900:9900 \ + phantom-talk \ + run dev diff --git a/scripts/run b/scripts/run new file mode 100755 index 0000000..8f33755 --- /dev/null +++ b/scripts/run @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +dir=$(dirname $0) +source $dir/common + +docker run \ + --volume $(abspath $dir/..):/opt/phantom-talk \ + --publish 9900:9900 \ + phantom-talk \ + run deck diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..bf0b4b8 --- /dev/null +++ b/scripts/test @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +dir=$(dirname $0) +source $dir/common + +docker run \ + --volume $(abspath $dir/..):/opt/phantom-talk \ + --publish 9900:9900 \ + phantom-talk \ + test diff --git a/test/app/app.js b/test/app/app.js new file mode 100644 index 0000000..31a6468 --- /dev/null +++ b/test/app/app.js @@ -0,0 +1,15 @@ +define(['app/app'], function (App) { + describe('App', function () { + it('→ exits`', function () { + expect(new App()).not.toBeUndefined(); + }); + + describe('→ start()`', function () { + it('→ can be called', function () { + expect(function noError() { + (new App()).start(); + }).not.toThrow(); + }); + }); + }); +}); diff --git a/test/test-main.js b/test/test-main.js new file mode 100644 index 0000000..5e24d34 --- /dev/null +++ b/test/test-main.js @@ -0,0 +1,21 @@ +(function (root) { + 'use strict'; + + var testDir = /base\/test/; + var isTestMain = /test-main.js$/; + + var tests = Object.keys(root.__karma__.files).filter(function (file) { + return testDir.test(file) && !isTestMain.test(file); + }); + + require(['config'], function configRequire(mkConfig) { + var config = mkConfig('/base'); + + config.deps = tests; + config.baseUrl = '/base/static'; + config.callback = root.__karma__.start; + + require.config(config); + }); + +}(this)); diff --git a/vendor/backbone.babysitter/.bower.json b/vendor/backbone.babysitter/.bower.json new file mode 100644 index 0000000..785899c --- /dev/null +++ b/vendor/backbone.babysitter/.bower.json @@ -0,0 +1,40 @@ +{ + "name": "backbone.babysitter", + "version": "0.1.6", + "homepage": "https://github.com/marionettejs/backbone.babysitter", + "authors": [ + "Derick Bailey " + ], + "description": "Manage child views in a Backbone.View", + "main": "lib/backbone.babysitter.js", + "keywords": [ + "backbone", + "plugin", + "computed", + "field", + "model", + "client", + "browser" + ], + "dependencies": { + "backbone": ">=0.9.9 <=1.1.2", + "underscore": ">=1.4.0 <=1.6.0" + }, + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "_release": "0.1.6", + "_resolution": { + "type": "version", + "tag": "v0.1.6", + "commit": "13af3ea9675368ba34b3aff3ba8ff0ecb03461c4" + }, + "_source": "git://github.com/marionettejs/backbone.babysitter.git", + "_target": "^0.1.0", + "_originalSource": "backbone.babysitter" +} \ No newline at end of file diff --git a/vendor/backbone.babysitter/CHANGELOG.md b/vendor/backbone.babysitter/CHANGELOG.md new file mode 100644 index 0000000..516cdc4 --- /dev/null +++ b/vendor/backbone.babysitter/CHANGELOG.md @@ -0,0 +1,59 @@ +# Change log + +### v0.1.6 + +* Expose `reduce` to babysitter collections. Thanks @romanbsd + +### v0.1.5 + +* Minor updates to bower.json + +### v0.1.4 + +* Update UMD Wrapper and build process + +### v0.1.2 + +* Add .VERSION and n.oConflict +* General cleanups to tests and package.json +* Add travis build info + +### v0.1.1 +* Remove AMD builds and replace with a single UMD style wrapper. + +### v0.1.0 +* allow chaining of add and remove methods +* add component.json + +#### General +* update grunt file +* readme fixed +* fix gruntfile url + +### v0.0.6 + +* Removed `.findByCollection` method +* Added `.findByModelCid` method + +### v0.0.5 + +* Updated build process to use GruntJS v0.4 + +### v0.0.4 + +* Added a fix for IE < 9, when applying a function to the views +* Added `.pluck` as a method, from Underscore.js +* Can specify an array of views to the container constructor + +### v0.0.3 + +* Added iterators and other collection processing functions from Underscore.js + +### v0.0.2 + +* Added `.length` attribute +* Added `.findByIndex` method + +### v0.0.1 + +* Initial release diff --git a/vendor/backbone.babysitter/Gruntfile.js b/vendor/backbone.babysitter/Gruntfile.js new file mode 100644 index 0000000..29a9939 --- /dev/null +++ b/vendor/backbone.babysitter/Gruntfile.js @@ -0,0 +1,130 @@ +/*global module:false*/ +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + meta: { + version: '<%= pkg.version %>', + banner: + '// Backbone.BabySitter\n' + + '// -------------------\n' + + '// v<%= pkg.version %>\n' + + '//\n' + + '// Copyright (c)<%= grunt.template.today("yyyy") %> Derick Bailey, Muted Solutions, LLC.\n' + + '// Distributed under MIT license\n' + + '//\n' + + '// http://github.com/marionettejs/backbone.babysitter\n' + + '\n' + }, + + lint: { + files: ['src/*.js'] + }, + + preprocess: { + umd: { + src: 'src/build/backbone.babysitter.js', + dest: 'lib/backbone.babysitter.js' + } + }, + + template: { + options: { + data: { + version: '<%= meta.version %>' + } + }, + umd: { + src: '<%= preprocess.umd.dest %>', + dest: '<%= preprocess.umd.dest %>' + } + }, + + concat: { + options: { + banner: "<%= meta.banner %>" + }, + umd: { + src: '<%= preprocess.umd.dest %>', + dest: '<%= preprocess.umd.dest %>' + } + }, + + uglify : { + options: { + banner: "<%= meta.banner %>" + }, + umd : { + src : 'lib/backbone.babysitter.js', + dest : 'lib/backbone.babysitter.min.js', + options : { + sourceMap : 'lib/backbone.babysitter.map', + sourceMappingURL : 'backbone.babysitter.map', + sourceMapPrefix : 2 + } + } + }, + + jasmine : { + options : { + helpers : 'spec/javascripts/helpers/*.js', + specs : 'spec/javascripts/**/*.spec.js', + vendor : [ + 'public/javascripts/jquery.js', + 'public/javascripts/json2.js', + 'public/javascripts/underscore.js', + 'public/javascripts/backbone.js' + ], + }, + babysitter : { + src : ['src/*.js'] + } + }, + + jshint: { + options: { + jshintrc : '.jshintrc' + }, + babysitter : [ 'src/*.js' ] + }, + + watch: { + babysitter : { + files : ['src/*.js', 'spec/**/*.js'], + tasks : ['jshint', 'jasmine:babysitter'] + }, + server : { + files : ['src/*.js', 'spec/**/*.js'], + tasks : ['jasmine:babysitter:build'] + } + }, + + connect: { + server: { + options: { + port: 8888 + } + } + } + }); + + grunt.loadNpmTasks('grunt-preprocess'); + grunt.loadNpmTasks('grunt-template'); + grunt.loadNpmTasks('grunt-contrib-jasmine'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-connect'); + + grunt.registerTask('test', ['jshint', 'jasmine:babysitter']); + + grunt.registerTask('dev', ['test', 'watch:babysitter']); + + grunt.registerTask('server', ['jasmine:babysitter:build', 'connect:server', 'watch:server']); + + // Default task. + grunt.registerTask('default', ['jshint', 'jasmine:babysitter', 'preprocess', 'template', 'concat', 'uglify']); + +}; diff --git a/vendor/backbone.babysitter/LICENSE.md b/vendor/backbone.babysitter/LICENSE.md new file mode 100644 index 0000000..1ec483c --- /dev/null +++ b/vendor/backbone.babysitter/LICENSE.md @@ -0,0 +1,5 @@ +# Backbone.BabySitter + +Copyright (C)2013 Derick Bailey, Muted Solutions, LLC + +Distributed Under [MIT License](http://mutedsolutions.mit-license.org/) diff --git a/vendor/backbone.babysitter/bower.json b/vendor/backbone.babysitter/bower.json new file mode 100644 index 0000000..2bff53f --- /dev/null +++ b/vendor/backbone.babysitter/bower.json @@ -0,0 +1,31 @@ +{ + "name": "backbone.babysitter", + "version": "0.1.6", + "homepage": "https://github.com/marionettejs/backbone.babysitter", + "authors": [ + "Derick Bailey " + ], + "description": "Manage child views in a Backbone.View", + "main": "lib/backbone.babysitter.js", + "keywords": [ + "backbone", + "plugin", + "computed", + "field", + "model", + "client", + "browser" + ], + "dependencies": { + "backbone": ">=0.9.9 <=1.1.2", + "underscore": ">=1.4.0 <=1.6.0" + }, + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/vendor/backbone.babysitter/component.json b/vendor/backbone.babysitter/component.json new file mode 100644 index 0000000..61fe472 --- /dev/null +++ b/vendor/backbone.babysitter/component.json @@ -0,0 +1,38 @@ +{ + "name": "backbone.babysitter", + "description": "Manage child views in a Backbone.View", + "version": "0.1.4", + "repo": "marionettejs/backbone.babysitter", + "main": "lib/backbone.babysitter.js", + "keywords": [ + "backbone", + "plugin", + "computed", + "field", + "model", + "client", + "browser" + ], + "license": "MIT", + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/marionettejs/backbone.babysitter/blob/master/LICENSE.md" + } + ], + + "scripts": [ + "lib/backbone.babysitter.js" + ], + + "author": { + "name": "Derick Bailey", + "email": "marionettejs@gmail.com", + "web": "http://derickbailey.lostechies.com" + }, + + "dependencies": { + "jashkenas/backbone": "*", + "jashkenas/underscore": "*" + } +} diff --git a/vendor/backbone.babysitter/lib/backbone.babysitter.js b/vendor/backbone.babysitter/lib/backbone.babysitter.js new file mode 100644 index 0000000..176ad80 --- /dev/null +++ b/vendor/backbone.babysitter/lib/backbone.babysitter.js @@ -0,0 +1,190 @@ +// Backbone.BabySitter +// ------------------- +// v0.1.6 +// +// Copyright (c)2015 Derick Bailey, Muted Solutions, LLC. +// Distributed under MIT license +// +// http://github.com/marionettejs/backbone.babysitter + +(function(root, factory) { + + if (typeof define === 'function' && define.amd) { + define(['backbone', 'underscore'], function(Backbone, _) { + return factory(Backbone, _); + }); + } else if (typeof exports !== 'undefined') { + var Backbone = require('backbone'); + var _ = require('underscore'); + module.exports = factory(Backbone, _); + } else { + factory(root.Backbone, root._); + } + +}(this, function(Backbone, _) { + 'use strict'; + + var previousChildViewContainer = Backbone.ChildViewContainer; + + // BabySitter.ChildViewContainer + // ----------------------------- + // + // Provide a container to store, retrieve and + // shut down child views. + + Backbone.ChildViewContainer = (function (Backbone, _) { + + // Container Constructor + // --------------------- + + var Container = function(views){ + this._views = {}; + this._indexByModel = {}; + this._indexByCustom = {}; + this._updateLength(); + + _.each(views, this.add, this); + }; + + // Container Methods + // ----------------- + + _.extend(Container.prototype, { + + // Add a view to this container. Stores the view + // by `cid` and makes it searchable by the model + // cid (and model itself). Optionally specify + // a custom key to store an retrieve the view. + add: function(view, customIndex){ + var viewCid = view.cid; + + // store the view + this._views[viewCid] = view; + + // index it by model + if (view.model){ + this._indexByModel[view.model.cid] = viewCid; + } + + // index by custom + if (customIndex){ + this._indexByCustom[customIndex] = viewCid; + } + + this._updateLength(); + return this; + }, + + // Find a view by the model that was attached to + // it. Uses the model's `cid` to find it. + findByModel: function(model){ + return this.findByModelCid(model.cid); + }, + + // Find a view by the `cid` of the model that was attached to + // it. Uses the model's `cid` to find the view `cid` and + // retrieve the view using it. + findByModelCid: function(modelCid){ + var viewCid = this._indexByModel[modelCid]; + return this.findByCid(viewCid); + }, + + // Find a view by a custom indexer. + findByCustom: function(index){ + var viewCid = this._indexByCustom[index]; + return this.findByCid(viewCid); + }, + + // Find by index. This is not guaranteed to be a + // stable index. + findByIndex: function(index){ + return _.values(this._views)[index]; + }, + + // retrieve a view by its `cid` directly + findByCid: function(cid){ + return this._views[cid]; + }, + + // Remove a view + remove: function(view){ + var viewCid = view.cid; + + // delete model index + if (view.model){ + delete this._indexByModel[view.model.cid]; + } + + // delete custom index + _.any(this._indexByCustom, function(cid, key) { + if (cid === viewCid) { + delete this._indexByCustom[key]; + return true; + } + }, this); + + // remove the view from the container + delete this._views[viewCid]; + + // update the length + this._updateLength(); + return this; + }, + + // Call a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.call`. + call: function(method){ + this.apply(method, _.tail(arguments)); + }, + + // Apply a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.apply`. + apply: function(method, args){ + _.each(this._views, function(view){ + if (_.isFunction(view[method])){ + view[method].apply(view, args || []); + } + }); + }, + + // Update the `.length` attribute on this container + _updateLength: function(){ + this.length = _.size(this._views); + } + }); + + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-106 + // + // Mix in methods from Underscore, for iteration, and other + // collection related features. + var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', + 'select', 'reject', 'every', 'all', 'some', 'any', 'include', + 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', + 'last', 'without', 'isEmpty', 'pluck', 'reduce']; + + _.each(methods, function(method) { + Container.prototype[method] = function() { + var views = _.values(this._views); + var args = [views].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; + }); + + // return the public API + return Container; + })(Backbone, _); + + + Backbone.ChildViewContainer.VERSION = '0.1.6'; + + Backbone.ChildViewContainer.noConflict = function () { + Backbone.ChildViewContainer = previousChildViewContainer; + return this; + }; + + return Backbone.ChildViewContainer; + +})); diff --git a/vendor/backbone.babysitter/lib/backbone.babysitter.map b/vendor/backbone.babysitter/lib/backbone.babysitter.map new file mode 100644 index 0000000..ddf2dae --- /dev/null +++ b/vendor/backbone.babysitter/lib/backbone.babysitter.map @@ -0,0 +1 @@ +{"version":3,"sources":["?"],"names":["root","factory","define","amd","Backbone","_","exports","require","module","this","previousChildViewContainer","ChildViewContainer","Container","views","_views","_indexByModel","_indexByCustom","_updateLength","each","add","extend","prototype","view","customIndex","viewCid","cid","model","findByModel","findByModelCid","modelCid","findByCid","findByCustom","index","findByIndex","values","remove","any","key","call","method","apply","tail","arguments","args","isFunction","length","size","methods","concat","toArray","VERSION","noConflict"],"mappings":"CASC,SAASA,EAAMC,GAEd,GAAsB,kBAAXC,SAAyBA,OAAOC,IACzCD,QAAQ,WAAY,cAAe,SAASE,EAAUC,GACpD,MAAOJ,GAAQG,EAAUC,SAEtB,IAAuB,mBAAZC,SAAyB,CACzC,GAAIF,GAAWG,QAAQ,YACnBF,EAAIE,QAAQ,aAChBC,QAAOF,QAAUL,EAAQG,EAAUC,OAEnCJ,GAAQD,EAAKI,SAAUJ,EAAKK,KAG9BI,KAAM,SAASL,EAAUC,GACzB,YAEA,IAAIK,GAA6BN,EAASO,kBAiK1C,OAzJAP,GAASO,mBAAqB,SAAWP,EAAUC,GAKjD,GAAIO,GAAY,SAASC,GACvBJ,KAAKK,UACLL,KAAKM,iBACLN,KAAKO,kBACLP,KAAKQ,gBAELZ,EAAEa,KAAKL,EAAOJ,KAAKU,IAAKV,MAM1BJ,GAAEe,OAAOR,EAAUS,WAMjBF,IAAK,SAASG,EAAMC,GAClB,GAAIC,GAAUF,EAAKG,GAgBnB,OAbAhB,MAAKK,OAAOU,GAAWF,EAGnBA,EAAKI,QACPjB,KAAKM,cAAcO,EAAKI,MAAMD,KAAOD,GAInCD,IACFd,KAAKO,eAAeO,GAAeC,GAGrCf,KAAKQ,gBACER,MAKTkB,YAAa,SAASD,GACpB,MAAOjB,MAAKmB,eAAeF,EAAMD,MAMnCG,eAAgB,SAASC,GACvB,GAAIL,GAAUf,KAAKM,cAAcc,EACjC,OAAOpB,MAAKqB,UAAUN,IAIxBO,aAAc,SAASC,GACrB,GAAIR,GAAUf,KAAKO,eAAegB,EAClC,OAAOvB,MAAKqB,UAAUN,IAKxBS,YAAa,SAASD,GACpB,MAAO3B,GAAE6B,OAAOzB,KAAKK,QAAQkB,IAI/BF,UAAW,SAASL,GAClB,MAAOhB,MAAKK,OAAOW,IAIrBU,OAAQ,SAASb,GACf,GAAIE,GAAUF,EAAKG,GAoBnB,OAjBIH,GAAKI,aACAjB,MAAKM,cAAcO,EAAKI,MAAMD,KAIvCpB,EAAE+B,IAAI3B,KAAKO,eAAgB,SAASS,EAAKY,GACvC,MAAIZ,KAAQD,SACHf,MAAKO,eAAeqB,IACpB,GAFT,QAIC5B,YAGIA,MAAKK,OAAOU,GAGnBf,KAAKQ,gBACER,MAMT6B,KAAM,SAASC,GACb9B,KAAK+B,MAAMD,EAAQlC,EAAEoC,KAAKC,aAM5BF,MAAO,SAASD,EAAQI,GACtBtC,EAAEa,KAAKT,KAAKK,OAAQ,SAASQ,GACvBjB,EAAEuC,WAAWtB,EAAKiB,KACpBjB,EAAKiB,GAAQC,MAAMlB,EAAMqB,UAM/B1B,cAAe,WACbR,KAAKoC,OAASxC,EAAEyC,KAAKrC,KAAKK,UAS9B,IAAIiC,IAAW,UAAW,OAAQ,MAAO,OAAQ,SAAU,SACzD,SAAU,SAAU,QAAS,MAAO,OAAQ,MAAO,UACnD,WAAY,SAAU,UAAW,QAAS,UAAW,OACrD,OAAQ,UAAW,UAAW,QAAS,SAWzC,OATA1C,GAAEa,KAAK6B,EAAS,SAASR,GACvB3B,EAAUS,UAAUkB,GAAU,WAC5B,GAAI1B,GAAQR,EAAE6B,OAAOzB,KAAKK,QACtB6B,GAAQ9B,GAAOmC,OAAO3C,EAAE4C,QAAQP,WACpC,OAAOrC,GAAEkC,GAAQC,MAAMnC,EAAGsC,MAKvB/B,GACNR,EAAUC,GAGbD,EAASO,mBAAmBuC,QAAU,QAEtC9C,EAASO,mBAAmBwC,WAAa,WAEvC,MADA/C,GAASO,mBAAqBD,EACvBD,MAGFL,EAASO","file":"lib/backbone.babysitter.min.js"} \ No newline at end of file diff --git a/vendor/backbone.babysitter/lib/backbone.babysitter.min.js b/vendor/backbone.babysitter/lib/backbone.babysitter.min.js new file mode 100644 index 0000000..51b64cf --- /dev/null +++ b/vendor/backbone.babysitter/lib/backbone.babysitter.min.js @@ -0,0 +1,11 @@ +// Backbone.BabySitter +// ------------------- +// v0.1.6 +// +// Copyright (c)2015 Derick Bailey, Muted Solutions, LLC. +// Distributed under MIT license +// +// http://github.com/marionettejs/backbone.babysitter + +(function(i,e){if("function"==typeof define&&define.amd)define(["backbone","underscore"],function(i,t){return e(i,t)});else if("undefined"!=typeof exports){var t=require("backbone"),n=require("underscore");module.exports=e(t,n)}else e(i.Backbone,i._)})(this,function(i,e){"use strict";var t=i.ChildViewContainer;return i.ChildViewContainer=function(i,e){var t=function(i){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),e.each(i,this.add,this)};e.extend(t.prototype,{add:function(i,e){var t=i.cid;return this._views[t]=i,i.model&&(this._indexByModel[i.model.cid]=t),e&&(this._indexByCustom[e]=t),this._updateLength(),this},findByModel:function(i){return this.findByModelCid(i.cid)},findByModelCid:function(i){var e=this._indexByModel[i];return this.findByCid(e)},findByCustom:function(i){var e=this._indexByCustom[i];return this.findByCid(e)},findByIndex:function(i){return e.values(this._views)[i]},findByCid:function(i){return this._views[i]},remove:function(i){var t=i.cid;return i.model&&delete this._indexByModel[i.model.cid],e.any(this._indexByCustom,function(i,e){return i===t?(delete this._indexByCustom[e],!0):void 0},this),delete this._views[t],this._updateLength(),this},call:function(i){this.apply(i,e.tail(arguments))},apply:function(i,t){e.each(this._views,function(n){e.isFunction(n[i])&&n[i].apply(n,t||[])})},_updateLength:function(){this.length=e.size(this._views)}});var n=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck","reduce"];return e.each(n,function(i){t.prototype[i]=function(){var t=e.values(this._views),n=[t].concat(e.toArray(arguments));return e[i].apply(e,n)}}),t}(i,e),i.ChildViewContainer.VERSION="0.1.6",i.ChildViewContainer.noConflict=function(){return i.ChildViewContainer=t,this},i.ChildViewContainer}); +//@ sourceMappingURL=backbone.babysitter.map \ No newline at end of file diff --git a/vendor/backbone.babysitter/package.json b/vendor/backbone.babysitter/package.json new file mode 100644 index 0000000..1c74611 --- /dev/null +++ b/vendor/backbone.babysitter/package.json @@ -0,0 +1,56 @@ +{ + "name": "backbone.babysitter", + "description": "Manage child views in a Backbone.View", + "version": "0.1.6", + "homepage": "https://github.com/marionettejs/backbone.babysitter", + "main": "lib/backbone.babysitter.js", + "keywords": [ + "backbone", + "plugin", + "computed", + "field", + "model", + "client", + "browser" + ], + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/marionettejs/backbone.babysitter/blob/master/LICENSE.md" + } + ], + "scripts": { + "test": "grunt jasmine", + "start": "grunt jasmine-server", + "build": "grunt" + }, + "author": { + "name": "Derick Bailey", + "email": "marionettejs@gmail.com", + "web": "http://derickbailey.lostechies.com" + }, + "bugs": { + "url": "https://github.com/marionettejs/backbone.babysitter/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/marionettejs/backbone.babysitter.git" + }, + "github": "https://github.com/marionettejs/backbone.babysitter", + "dependencies": { + "backbone": ">=0.9.9 <=1.1.2", + "underscore": ">=1.4.0 <=1.6.0" + }, + "devDependencies": { + "grunt": "0.4.4", + "grunt-cli": "0.1.13", + "grunt-contrib-concat": "0.1.2", + "grunt-contrib-connect": "0.1.2", + "grunt-contrib-jasmine": "0.6.4", + "grunt-contrib-jshint": "0.1.1", + "grunt-contrib-uglify": "0.1.1", + "grunt-contrib-watch": "0.2.0", + "grunt-preprocess": "4.0.0", + "grunt-template": "0.2.3" + } +} diff --git a/vendor/backbone.babysitter/public/javascripts/backbone.js b/vendor/backbone.babysitter/public/javascripts/backbone.js new file mode 100644 index 0000000..3512d42 --- /dev/null +++ b/vendor/backbone.babysitter/public/javascripts/backbone.js @@ -0,0 +1,1571 @@ +// Backbone.js 1.0.0 + +// (c) 2010-2013 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, `exports` + // 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 local references to array methods we'll want to use later. + var array = []; + var push = array.push; + var slice = array.slice; + var splice = array.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both the browser and the server. + 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 = '1.0.0'; + + // 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, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; + + // 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 + // --------------- + + // 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 an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on: function(name, callback, context) { + if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; + this._events || (this._events = {}); + var events = this._events[name] || (this._events[name] = []); + events.push({callback: callback, context: context, ctx: context || this}); + return this; + }, + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. + once: function(name, callback, context) { + if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; + var self = this; + var once = _.once(function() { + self.off(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + return this.on(name, once, context); + }, + + // 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 `name` is null, removes all bound + // callbacks for all events. + off: function(name, callback, context) { + var retain, ev, events, names, i, l, j, k; + if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; + if (!name && !callback && !context) { + this._events = {}; + return this; + } + + names = name ? [name] : _.keys(this._events); + for (i = 0, l = names.length; i < l; i++) { + name = names[i]; + if (events = this._events[name]) { + this._events[name] = retain = []; + if (callback || context) { + for (j = 0, k = events.length; j < k; j++) { + ev = events[j]; + if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || + (context && context !== ev.context)) { + retain.push(ev); + } + } + } + if (!retain.length) delete this._events[name]; + } + } + + 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(name) { + if (!this._events) return this; + var args = slice.call(arguments, 1); + if (!eventsApi(this, 'trigger', name, args)) return this; + var events = this._events[name]; + var allEvents = this._events.all; + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); + return this; + }, + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function(obj, name, callback) { + var listeners = this._listeners; + if (!listeners) return this; + var deleteListener = !name && !callback; + if (typeof name === 'object') callback = this; + if (obj) (listeners = {})[obj._listenerId] = obj; + for (var id in listeners) { + listeners[id].off(name, callback, this); + if (deleteListener) delete this._listeners[id]; + } + return this; + } + + }; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + + // Handle event maps. + if (typeof name === 'object') { + for (var key in name) { + obj[action].apply(obj, [key, name[key]].concat(rest)); + } + return false; + } + + // Handle space separated event names. + if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [names[i]].concat(rest)); + } + return false; + } + + return true; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + } + }; + + var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; + + // Inversion-of-control versions of `on` and `once`. Tell *this* object to + // listen to an event in another object ... keeping track of what it's + // listening to. + _.each(listenMethods, function(implementation, method) { + Events[method] = function(obj, name, callback) { + var listeners = this._listeners || (this._listeners = {}); + var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); + listeners[id] = obj; + if (typeof name === 'object') callback = this; + obj[implementation](name, callback, this); + return this; + }; + }); + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId('c'); + this.attributes = {}; + _.extend(this, _.pick(options, modelOptions)); + if (options.parse) attrs = this.parse(attrs, options) || {}; + if (defaults = _.result(this, 'defaults')) { + attrs = _.defaults({}, attrs, defaults); + } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // A list of options to be attached directly to the model, if provided. + var modelOptions = ['url', 'urlRoot', 'collection']; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: 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); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // 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"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // 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 (attr == null) 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; + var old = this._changing ? this._previousAttributes : this.attributes; + 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 (attr == null || !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); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overridden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + if (!model.set(model.parse(resp, options), options)) return false; + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('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, val, options) { + var attrs, method, xhr, attributes = this.attributes; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. + if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; + + options = _.extend({validate: true}, options); + + // Do not persist invalid models. + if (!this._validate(attrs, options)) return false; + + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); + + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; + + 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 destroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (options.wait || model.isNew()) destroy(); + if (success) success(model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + if (this.isNew()) { + options.success(); + return false; + } + wrapError(this, options); + + var xhr = this.sync('delete', this, options); + if (!options.wait) destroy(); + 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 = _.result(this, 'urlRoot') || _.result(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, options) { + 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; + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend(options || {}, { validate: true })); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); + return false; + } + + }); + + // Underscore methods that we want to implement on the Model. + var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + _.each(modelMethods, function(method) { + Model.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.attributes); + return _[method].apply(_, args); + }; + }); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analagous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // 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.url) this.url = options.url; + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, merge: false, remove: false}; + + // 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); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. + add: function(models, options) { + return this.set(models, _.defaults(options || {}, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + models = _.isArray(models) ? models.slice() : [models]; + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[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; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + options = _.defaults(options || {}, setOptions); + if (options.parse) models = this.parse(models, options); + if (!_.isArray(models)) models = models ? [models] : []; + var i, l, model, attrs, existing, sort; + var at = options.at; + var sortable = this.comparator && (at == null) && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + var toAdd = [], toRemove = [], modelMap = {}; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + for (i = 0, l = models.length; i < l; i++) { + if (!(model = this._prepareModel(models[i], options))) continue; + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + if (existing = this.get(model)) { + if (options.remove) modelMap[existing.cid] = true; + if (options.merge) { + existing.set(model.attributes, options); + if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + } + + // This is a new model, push it to the `toAdd` list. + } else if (options.add) { + toAdd.push(model); + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on('all', this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + } + + // Remove nonexistent models if appropriate. + if (options.remove) { + for (i = 0, l = this.length; i < l; ++i) { + if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + } + if (toRemove.length) this.remove(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + if (toAdd.length) { + if (sortable) sort = true; + this.length += toAdd.length; + if (at != null) { + splice.apply(this.models, [at, 0].concat(toAdd)); + } else { + push.apply(this.models, toAdd); + } + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + if (options.silent) return this; + + // Trigger `add` events. + for (i = 0, l = toAdd.length; i < l; i++) { + (model = toAdd[i]).trigger('add', model, this, options); + } + + // Trigger `sort` if the collection was sorted. + if (sort) this.trigger('sort', this, options); + return this; + }, + + // 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 granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + options.previousModels = this.models; + this._reset(); + this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: this.length}, 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; + }, + + // Slice out a sub-array of models from the collection. + slice: function(begin, end) { + return this.models.slice(begin, end); + }, + + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj.id != null ? obj.id : obj.cid || obj]; + }, + + // 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, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return this[first ? 'find' : 'filter'](function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, 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) { + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + // Run sort based on type of `comparator`. + if (_.isString(this.comparator) || this.comparator.length === 1) { + this.models = this.sortBy(this.comparator, this); + } else { + this.models.sort(_.bind(this.comparator, this)); + } + + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Figure out the smallest index at which a model should be inserted so as + // to maintain order. + sortedIndex: function(model, value, context) { + value || (value = this.comparator); + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _.sortedIndex(this.models, model, iterator, context); + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, 'get', attr); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success(collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('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) { + options = options ? _.clone(options) : {}; + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(resp) { + if (options.wait) collection.add(model, options); + if (success) success(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, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (attrs instanceof Model) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options || (options = {}); + options.collection = this; + var model = new this.model(attrs, options); + if (!model._validate(attrs, options)) { + this.trigger('invalid', this, attrs, options); + return false; + } + return model; + }, + + // Internal method to sever 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)]; + if (model.id != null) this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', + 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', + 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', + 'isEmpty', 'chain']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.models); + return _[method].apply(_, args); + }; + }); + + // Underscore methods that take a property name as an argument. + var attributeMethods = ['groupBy', 'countBy', 'sortBy']; + + // Use attributes instead of properties. + _.each(attributeMethods, function(method) { + Collection.prototype[method] = function(value, context) { + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _[method](this.models, iterator, context); + }; + }); + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + this._configure(options || {}); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be prefered to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this.$el.remove(); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save' + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; + if (selector === '') { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // Performs the initial configuration of a View with a set of options. + // Keys with special meaning *(e.g. model, collection, id, className)* are + // attached directly to the view. See `viewOptions` for an exhaustive + // list. + _configure: function(options) { + if (this.options) options = _.extend({}, _.result(this, 'options'), options); + _.extend(this, _.pick(options, viewOptions)); + this.options = options; + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === 'PATCH' && window.ActiveXObject && + !(window.external && window.external.msActiveXFilteringEnabled)) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, 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 optionalParam = /\((.*?)\)/g; + 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) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // 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; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional){ + return optional ? match : '([^\/]+)'; + }) + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + + // 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(window) { + var match = (window || this).location.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 || !this._wantsHashChange || forcePushState) { + fragment = this.location.pathname; + var root = this.root.replace(trailingSlash, ''); + if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); + } else { + fragment = this.getHash(); + } + } + 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.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + if (oldIE && this._wantsHashChange) { + this.iframe = Backbone.$('