├── .gitignore ├── .travis.yml ├── CHANGELOG ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── backbone-on-rails.gemspec ├── lib ├── backbone-on-rails.rb ├── backbone-on-rails │ ├── engine.rb │ └── version.rb └── generators │ └── backbone │ ├── helpers.rb │ ├── install │ ├── install_generator.rb │ └── templates │ │ ├── app.js │ │ └── app.js.coffee │ └── scaffold │ ├── scaffold_generator.rb │ └── templates │ ├── collection.js │ ├── collection.js.coffee │ ├── model.js │ ├── model.js.coffee │ ├── router.js │ ├── router.js.coffee │ ├── template.hbs │ ├── template.jst.eco │ ├── template.jst.ejs │ ├── view.js │ └── view.js.coffee ├── test ├── backbone-on-rails_test.rb ├── dummy │ ├── README.rdoc │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ └── application_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── mailers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ └── views │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── backtrace_silencers.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ └── routes.rb │ ├── db │ │ └── test.sqlite3 │ ├── lib │ │ └── assets │ │ │ └── .gitkeep │ ├── log │ │ └── .gitkeep │ ├── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── favicon.ico │ └── script │ │ └── rails ├── generators │ ├── install_generator_test.rb │ └── scaffold_generator_test.rb └── test_helper.rb └── vendor └── assets └── javascripts ├── backbone.js └── underscore.js /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | pkg/ 3 | test/dummy/log/ 4 | test/dummy/tmp/ 5 | test/tmp/ 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.2.0 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 1.2.0.0 (Jun 01, 2015) 5 | -------------------- 6 | - Add optional app name and path to generators (#70) 7 | - Update Backbone to 1.2.0 8 | - Update Underscore to 1.8.3 9 | 10 | 1.1.2.1 (Mar 25, 2015) 11 | -------------------- 12 | - Only depend on railties (#60) 13 | - Update Underscore to 1.8.2 14 | 15 | 1.1.2.0 (Sep 26, 2014) 16 | ---------------------- 17 | - Update Backbone to 1.1.2 (#65) 18 | - Update Underscore to 1.7.0 (#65) 19 | 20 | 1.1.1.0 (Feb 14, 2014 <3) 21 | ------------------------- 22 | - Update Backbone to 1.1.1 23 | - Update Underscore to 1.6.0 24 | - Enable usage with non-active_record projects (#52) 25 | 26 | 1.1.0.0 (Oct 14, 2013) 27 | ---------------------- 28 | - Update Backbone to 1.1.0 29 | - Update Underscore to 1.5.2 30 | 31 | 1.0.0.1 (Aug 19, 2013) 32 | ---------------------- 33 | - Update Underscore to 1.5.1 (#50) 34 | 35 | 1.0.0.0 (Mar 21, 2013) 36 | ---------------------- 37 | - Update Backbone to 1.1.0 (#47) 38 | - Update Underscore to 1.4.4 (#46) 39 | 40 | 0.9.10.0 (Jan 22, 2013) 41 | ---------------------- 42 | - Update Backbone to 0.9.10 (#42) 43 | 44 | 0.9.9.0 (Dec 14, 2012) 45 | ---------------------- 46 | - Update Backbone to 0.9.9 (#40) 47 | - Update Underscore to 1.4.3 (#40) 48 | 49 | 0.9.2.3 (Oct 25, 2012) 50 | ---------------------- 51 | - Update Underscore to 1.4.2 (#37) 52 | - Update file naming to match latest version of book (#33) 53 | - Install generator works on empty manifests (#16) 54 | 55 | 0.9.2.2 (Oct 21, 2012) 56 | ---------------------- 57 | - Remove `--namespace` option for generators 58 | - Update Underscore to 1.4.1 59 | - Use sentinel when injecting files into manifest 60 | - Don't lock ejs and eco gem versions 61 | - Unit tests 62 | 63 | 0.9.2.1 (Jul 12, 2012) 64 | ---------------------- 65 | - Update Underscore to 1.3.3 66 | 67 | 0.9.2.0 (Mar 22, 2012) 68 | ---------------------- 69 | - Update Backbone to 0.9.2 70 | - More Rails-y file naming conventions 71 | - Namespace option for generators 72 | - Set model option on collection 73 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Declare your gem's dependencies in backbone-on-rails.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # jquery-rails is used by the dummy application 9 | gem "jquery-rails" 10 | 11 | # Declare any dependencies that are still in development here instead of in 12 | # your gemspec. These might include edge Rails or gems from your path or 13 | # Git. Remember to move these dependencies to your gemspec before releasing 14 | # your gem to rubygems.org. 15 | 16 | # To use debugger 17 | # gem 'ruby-debug19', :require => 'ruby-debug' 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 William Meleyal 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Backbone on Rails 2 | ================= 3 | 4 | A simple gem for using Backbone.js with Rails. 5 | 6 | http://git.io/backbone-on-rails 7 | 8 | 9 | Features 10 | -------- 11 | 12 | * Vendors the latest Backbone.js + Underscore.js in the asset pipeline 13 | 14 | * Provides an install generator to create a skeleton directory 15 | structure and manifest 16 | 17 | * Provides a scaffold generator to create files and boilerplate 18 | 19 | * Uses the naming conventions from 20 | thoughtbot's [Backbone.js on Rails](http://bit.ly/pLsmzr) 21 | 22 | * Generates CoffeeScript (default) or JavaScript 23 | 24 | 25 | Usage 26 | ----- 27 | 28 | gem 'backbone-on-rails' 29 | 30 | bundle install 31 | 32 | rails generate backbone:install 33 | 34 | rails generate backbone:scaffold NAME 35 | 36 | *Note: Remember to restart the server after installing* 37 | 38 | See also: 39 | 40 | * http://railscasts.com/episodes/323-backbone-on-rails-part-1 41 | * http://railscasts.com/episodes/325-backbone-on-rails-part-2 42 | 43 | 44 | Output 45 | ------ 46 | 47 | Example output from backbone:scaffold planet 48 | 49 | app/assets/ 50 | ├── javascripts 51 | │ ├── application.js 52 | │ ├── space_app.js.coffee 53 | │ ├── collections 54 | │ │ └── planets.js.coffee 55 | │ ├── models 56 | │ │ └── planet.js.coffee 57 | │ ├── routers 58 | │ │ └── planets_router.js.coffee 59 | │ └── views 60 | │ └── planets 61 | │ └── planets_index.js.coffee 62 | └── templates 63 | └── planets 64 | └── index.jst.eco 65 | 66 | 67 | Tricks 68 | ------ 69 | 70 | # Custom manifest 71 | rails generate backbone:install --manifest index.js 72 | 73 | # Generate JavaScript 74 | rails generate backbone:install --javascript 75 | 76 | # Custom Appname 77 | rails generate backbone:install --app=CustomApp 78 | 79 | # Place code within a sub directory structure 80 | rails generate backbone:install --dir=custom_app 81 | 82 | # Remove generated files 83 | rails destroy backbone:scaffold planet 84 | 85 | # Create a custom app name 86 | rails generate backbone:scaffold planet -a=CustomApp 87 | 88 | # Generate scaffold in sub directory of assets/javascripts & assets/templates 89 | rails generate backbone:scaffold planet -d=custom_app 90 | 91 | # Use Handlebars File instead of JST 92 | rails generate backbone:scaffold planet --template=hbs 93 | 94 | # Use SHT instead of JST as template namespace 95 | rails generate backbone:scaffold planet --template_namespace=SHT 96 | 97 | Alternatives 98 | ------------ 99 | 100 | For other features check out: 101 | 102 | * https://github.com/codebrew/backbone-rails 103 | * https://github.com/aflatter/backbone-rails 104 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | Bundler::GemHelper.install_tasks 9 | 10 | require 'rake/testtask' 11 | 12 | Rake::TestTask.new(:test) do |t| 13 | t.libs << 'lib' 14 | t.libs << 'test' 15 | t.pattern = 'test/**/*_test.rb' 16 | t.verbose = false 17 | end 18 | 19 | task :default => :test 20 | -------------------------------------------------------------------------------- /backbone-on-rails.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "backbone-on-rails/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "backbone-on-rails" 9 | s.version = BackboneOnRails::VERSION 10 | s.authors = ["William Meleyal"] 11 | s.email = ["william.meleyal@gmail.com"] 12 | s.homepage = "http://github.com/meleyal/backbone-on-rails" 13 | s.summary = "A simple gem for using Backbone with Rails" 14 | s.description = "A simple gem for using Backbone with Rails (>= 3.1), based on thoughtbot's 'Backbone.js on Rails'" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test}/*`.split("\n") 18 | 19 | s.add_dependency 'railties' 20 | s.add_dependency 'jquery-rails' 21 | s.add_dependency 'ejs' 22 | s.add_dependency 'eco' 23 | 24 | s.add_development_dependency 'rails', '3.2' 25 | s.add_development_dependency 'sqlite3' 26 | s.add_development_dependency 'test-unit' 27 | s.add_development_dependency 'turn' 28 | end 29 | -------------------------------------------------------------------------------- /lib/backbone-on-rails.rb: -------------------------------------------------------------------------------- 1 | require 'backbone-on-rails/engine' 2 | require 'backbone-on-rails/version' -------------------------------------------------------------------------------- /lib/backbone-on-rails/engine.rb: -------------------------------------------------------------------------------- 1 | module BackboneOnRails 2 | class Engine < Rails::Engine 3 | end 4 | end -------------------------------------------------------------------------------- /lib/backbone-on-rails/version.rb: -------------------------------------------------------------------------------- 1 | module BackboneOnRails 2 | VERSION = "1.2.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/backbone/helpers.rb: -------------------------------------------------------------------------------- 1 | module Backbone 2 | module Generators 3 | module Helpers 4 | 5 | def asset_path 6 | File.join('app', 'assets') 7 | end 8 | 9 | def javascript_path 10 | File.join(asset_path, ['javascripts', @subdirname].compact.join('/')) 11 | end 12 | 13 | def model_path 14 | File.join(javascript_path, "models") 15 | end 16 | 17 | def collection_path 18 | File.join(javascript_path, "collections") 19 | end 20 | 21 | def router_path 22 | File.join(javascript_path, "routers") 23 | end 24 | 25 | def view_path 26 | File.join(javascript_path, "views") 27 | end 28 | 29 | def template_path 30 | File.join(asset_path, ['templates', @subdirname].compact.join('/')) 31 | end 32 | 33 | def singular_file_name 34 | "#{file_name.singularize}#{@ext}" 35 | end 36 | 37 | def plural_file_name 38 | "#{file_name.pluralize}#{@ext}" 39 | end 40 | 41 | def router_file_name 42 | "#{file_name.pluralize}_router#{@ext}" 43 | end 44 | 45 | def view_file_name 46 | "#{file_name.pluralize}_index#{@ext}" 47 | end 48 | 49 | def model_namespace 50 | [app_name, "Models", file_name.singularize.camelize].join(".") 51 | end 52 | 53 | def collection_namespace 54 | [app_name, "Collections", file_name.pluralize.camelize].join(".") 55 | end 56 | 57 | def router_namespace 58 | [app_name, "Routers", file_name.pluralize.camelize].join(".") 59 | end 60 | 61 | def view_namespace 62 | [app_name, "Views", "#{file_name.pluralize.camelize}Index"].join(".") 63 | end 64 | 65 | def template_namespace 66 | File.join([@subdirname, file_path.pluralize].compact.join('/'), "index") 67 | end 68 | 69 | def template_controller 70 | @template_controller ||= 'JST' 71 | end 72 | 73 | def app_name 74 | rails_app_name.camelize 75 | end 76 | 77 | def app_filename 78 | rails_app_name.underscore 79 | end 80 | 81 | def rails_app_name 82 | @rails_app_name ||= Rails.application.class.name.split('::').first 83 | end 84 | 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/generators/backbone/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'generators/backbone/helpers' 2 | 3 | module Backbone 4 | module Generators 5 | class InstallGenerator < Rails::Generators::Base 6 | include Backbone::Generators::Helpers 7 | 8 | source_root File.expand_path("../templates", __FILE__) 9 | 10 | desc "Generates a Backbone.js skeleton directory structure and manifest" 11 | 12 | class_option :javascript, 13 | type: :boolean, 14 | aliases: "-j", 15 | default: false, 16 | desc: "Generate JavaScript" 17 | 18 | class_option :manifest, 19 | type: :string, 20 | aliases: "-m", 21 | default: "application.js", 22 | desc: "Javascript manifest file to modify (or create)" 23 | 24 | class_option :app, 25 | type: :string, 26 | aliases: "-a", 27 | default: nil, 28 | desc: "Application Name" 29 | 30 | class_option :dir, 31 | type: :string, 32 | aliases: "-d", 33 | default: nil, 34 | desc: "Subdirectory for files" 35 | 36 | def setup 37 | @rails_app_name = options.app 38 | @subdirname = options.dir 39 | end 40 | 41 | def create_dir_layout 42 | empty_directory model_path 43 | empty_directory collection_path 44 | empty_directory router_path 45 | empty_directory view_path 46 | empty_directory template_path 47 | end 48 | 49 | def create_app_file 50 | js = options.javascript 51 | ext = js ? ".js" : ".js.coffee" 52 | template "app#{ext}", "#{javascript_path}/#{app_filename}#{ext}" 53 | end 54 | 55 | def inject_backbone 56 | manifest = File.join(javascript_path, options.manifest) 57 | libs = %w(underscore backbone) 58 | paths = %w(../templates ./models ./collections ./views ./routers) 59 | 60 | out = [] 61 | out << libs.map{ |lib| "//= require #{lib}" } 62 | out << "//= require #{app_filename}" 63 | out << paths.map{ |path| "//= require_tree #{path}" } 64 | out = out.join("\n") + "\n" 65 | 66 | in_root do 67 | create_file(manifest) unless File.exists?(manifest) 68 | if File.open(manifest).read().include?('//= require_tree') 69 | inject_into_file(manifest, out, before: '//= require_tree') 70 | else 71 | append_file(manifest, out) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/generators/backbone/install/templates/app.js: -------------------------------------------------------------------------------- 1 | window.<%= app_name %> = { 2 | Models: {}, 3 | Collections: {}, 4 | Views: {}, 5 | Routers: {}, 6 | initialize: function() { 7 | alert('Hello from Backbone!'); 8 | } 9 | }; 10 | 11 | $(document).ready(function(){ 12 | <%= app_name %>.initialize(); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/generators/backbone/install/templates/app.js.coffee: -------------------------------------------------------------------------------- 1 | window.<%= app_name %> = 2 | Models: {} 3 | Collections: {} 4 | Views: {} 5 | Routers: {} 6 | initialize: -> alert 'Hello from Backbone!' 7 | 8 | $(document).ready -> 9 | <%= app_name %>.initialize() 10 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/scaffold_generator.rb: -------------------------------------------------------------------------------- 1 | require 'generators/backbone/helpers' 2 | 3 | module Backbone 4 | module Generators 5 | class ScaffoldGenerator < Rails::Generators::NamedBase 6 | include Backbone::Generators::Helpers 7 | 8 | source_root File.expand_path("../templates", __FILE__) 9 | 10 | desc "Generates a Backbone.js resource scaffold" 11 | 12 | class_option :javascript, 13 | type: :boolean, 14 | aliases: "-j", 15 | default: false, 16 | desc: "Generate JavaScript" 17 | 18 | class_option :app, 19 | type: :string, 20 | aliases: "-a", 21 | default: nil, 22 | desc: "Application Name" 23 | 24 | class_option :dir, 25 | type: :string, 26 | aliases: "-d", 27 | default: nil, 28 | desc: "Subdirectory for files" 29 | 30 | class_option :template, 31 | type: :string, 32 | default: "ejs", 33 | desc: "Extension for template files" 34 | 35 | class_option :template_namespace, 36 | type: :string, 37 | default: "JST", 38 | desc: "Template namespace (ex: JST / SHT / and so on)" 39 | 40 | def parse_options 41 | js = options.javascript 42 | @ext = js ? ".js" : ".js.coffee" 43 | @rails_app_name = options.app 44 | @subdirname = options.dir 45 | end 46 | 47 | def parse_javascript_templates 48 | js = options.javascript 49 | 50 | # Add additional formats here if we'd like to extend. 51 | # => 2015/05/26 Added Handlebars Templates. 52 | # 53 | @jst = case options.template 54 | when 'hbs' 55 | 'hbs' 56 | else 57 | js ? 'jst.ejs' : 'jst.eco' 58 | end 59 | 60 | # Instead of re-writing the template portion of the JS repeatedly, adding an option to allow custom object name for templates 61 | @template_controller = options.template_namespace 62 | end 63 | 64 | def create_backbone_model 65 | file = File.join(model_path, singular_file_name) 66 | template "model#{@ext}", file 67 | end 68 | 69 | def create_backbone_collection 70 | file = File.join(collection_path, plural_file_name) 71 | template "collection#{@ext}", file 72 | end 73 | 74 | def create_backbone_router 75 | file = File.join(router_path, router_file_name) 76 | template "router#{@ext}", file 77 | end 78 | 79 | def create_backbone_view 80 | empty_directory File.join(view_path, file_name.pluralize) 81 | file = File.join(view_path, file_name.pluralize, view_file_name) 82 | template "view#{@ext}", file 83 | end 84 | 85 | def create_backbone_template 86 | empty_directory File.join(template_path, file_name.pluralize) 87 | file = File.join(template_path, file_name.pluralize, "index.#{@jst}") 88 | template "template.#{@jst}", file 89 | end 90 | 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/collection.js: -------------------------------------------------------------------------------- 1 | <%= collection_namespace %> = Backbone.Collection.extend({ 2 | 3 | model: <%= model_namespace %> 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/collection.js.coffee: -------------------------------------------------------------------------------- 1 | class <%= collection_namespace %> extends Backbone.Collection 2 | 3 | model: <%= model_namespace %> 4 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/model.js: -------------------------------------------------------------------------------- 1 | <%= model_namespace %> = Backbone.Model.extend({ 2 | 3 | }); 4 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/model.js.coffee: -------------------------------------------------------------------------------- 1 | class <%= model_namespace %> extends Backbone.Model 2 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/router.js: -------------------------------------------------------------------------------- 1 | <%= router_namespace %> = Backbone.Router.extend({ 2 | 3 | }); 4 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/router.js.coffee: -------------------------------------------------------------------------------- 1 | class <%= router_namespace %> extends Backbone.Router 2 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/template.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/lib/generators/backbone/scaffold/templates/template.hbs -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/template.jst.eco: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/lib/generators/backbone/scaffold/templates/template.jst.eco -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/template.jst.ejs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/lib/generators/backbone/scaffold/templates/template.jst.ejs -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/view.js: -------------------------------------------------------------------------------- 1 | <%= view_namespace %> = Backbone.View.extend({ 2 | 3 | template: <%= template_controller %>['<%= template_namespace %>'] 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /lib/generators/backbone/scaffold/templates/view.js.coffee: -------------------------------------------------------------------------------- 1 | class <%= view_namespace %> extends Backbone.View 2 | 3 | template: <%= template_controller %>['<%= template_namespace %>'] 4 | -------------------------------------------------------------------------------- /test/backbone-on-rails_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BackboneOnRailsTest < Rails::Generators::TestCase 4 | 5 | destination File.expand_path("../../tmp", __FILE__) 6 | 7 | def setup 8 | @app = Dummy::Application 9 | end 10 | 11 | test "underscore.js is found as an asset" do 12 | assert_not_nil @app.assets["underscore"] 13 | assert_file "#{BackboneOnRails::Engine.root}/vendor/assets/javascripts/underscore.js", /1.8.3/ 14 | end 15 | 16 | test "backbone.js is found as an asset" do 17 | assert_not_nil @app.assets["backbone"] 18 | assert_file "#{BackboneOnRails::Engine.root}/vendor/assets/javascripts/backbone.js", /1.2.0/ 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /test/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == Welcome to Rails 2 | 3 | Rails is a web-application framework that includes everything needed to create 4 | database-backed web applications according to the Model-View-Control pattern. 5 | 6 | This pattern splits the view (also called the presentation) into "dumb" 7 | templates that are primarily responsible for inserting pre-built data in between 8 | HTML tags. The model contains the "smart" domain objects (such as Account, 9 | Product, Person, Post) that holds all the business logic and knows how to 10 | persist themselves to a database. The controller handles the incoming requests 11 | (such as Save New Account, Update Product, Show Post) by manipulating the model 12 | and directing data to the view. 13 | 14 | In Rails, the model is handled by what's called an object-relational mapping 15 | layer entitled Active Record. This layer allows you to present the data from 16 | database rows as objects and embellish these data objects with business logic 17 | methods. You can read more about Active Record in 18 | link:files/vendor/rails/activerecord/README.html. 19 | 20 | The controller and view are handled by the Action Pack, which handles both 21 | layers by its two parts: Action View and Action Controller. These two layers 22 | are bundled in a single package due to their heavy interdependence. This is 23 | unlike the relationship between the Active Record and Action Pack that is much 24 | more separate. Each of these packages can be used independently outside of 25 | Rails. You can read more about Action Pack in 26 | link:files/vendor/rails/actionpack/README.html. 27 | 28 | 29 | == Getting Started 30 | 31 | 1. At the command prompt, create a new Rails application: 32 | rails new myapp (where myapp is the application name) 33 | 34 | 2. Change directory to myapp and start the web server: 35 | cd myapp; rails server (run with --help for options) 36 | 37 | 3. Go to http://localhost:3000/ and you'll see: 38 | "Welcome aboard: You're riding Ruby on Rails!" 39 | 40 | 4. Follow the guidelines to start developing your application. You can find 41 | the following resources handy: 42 | 43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html 44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ 45 | 46 | 47 | == Debugging Rails 48 | 49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that 50 | will help you debug it and get it back on the rails. 51 | 52 | First area to check is the application log files. Have "tail -f" commands 53 | running on the server.log and development.log. Rails will automatically display 54 | debugging and runtime information to these files. Debugging info will also be 55 | shown in the browser on requests from 127.0.0.1. 56 | 57 | You can also log your own messages directly into the log file from your code 58 | using the Ruby logger class from inside your controllers. Example: 59 | 60 | class WeblogController < ActionController::Base 61 | def destroy 62 | @weblog = Weblog.find(params[:id]) 63 | @weblog.destroy 64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") 65 | end 66 | end 67 | 68 | The result will be a message in your log file along the lines of: 69 | 70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! 71 | 72 | More information on how to use the logger is at http://www.ruby-doc.org/core/ 73 | 74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are 75 | several books available online as well: 76 | 77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) 78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) 79 | 80 | These two books will bring you up to speed on the Ruby language and also on 81 | programming in general. 82 | 83 | 84 | == Debugger 85 | 86 | Debugger support is available through the debugger command when you start your 87 | Mongrel or WEBrick server with --debugger. This means that you can break out of 88 | execution at any point in the code, investigate and change the model, and then, 89 | resume execution! You need to install ruby-debug to run the server in debugging 90 | mode. With gems, use sudo gem install ruby-debug. Example: 91 | 92 | class WeblogController < ActionController::Base 93 | def index 94 | @posts = Post.all 95 | debugger 96 | end 97 | end 98 | 99 | So the controller will accept the action, run the first line, then present you 100 | with a IRB prompt in the server window. Here you can do things like: 101 | 102 | >> @posts.inspect 103 | => "[#nil, "body"=>nil, "id"=>"1"}>, 105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" 107 | >> @posts.first.title = "hello from a debugger" 108 | => "hello from a debugger" 109 | 110 | ...and even better, you can examine how your runtime objects actually work: 111 | 112 | >> f = @posts.first 113 | => #nil, "body"=>nil, "id"=>"1"}> 114 | >> f. 115 | Display all 152 possibilities? (y or n) 116 | 117 | Finally, when you're ready to resume execution, you can enter "cont". 118 | 119 | 120 | == Console 121 | 122 | The console is a Ruby shell, which allows you to interact with your 123 | application's domain model. Here you'll have all parts of the application 124 | configured, just like it is when the application is running. You can inspect 125 | domain models, change values, and save to the database. Starting the script 126 | without arguments will launch it in the development environment. 127 | 128 | To start the console, run rails console from the application 129 | directory. 130 | 131 | Options: 132 | 133 | * Passing the -s, --sandbox argument will rollback any modifications 134 | made to the database. 135 | * Passing an environment name as an argument will load the corresponding 136 | environment. Example: rails console production. 137 | 138 | To reload your controllers and models after launching the console run 139 | reload! 140 | 141 | More information about irb can be found at: 142 | link:http://www.rubycentral.org/pickaxe/irb.html 143 | 144 | 145 | == dbconsole 146 | 147 | You can go to the command line of your database directly through rails 148 | dbconsole. You would be connected to the database with the credentials 149 | defined in database.yml. Starting the script without arguments will connect you 150 | to the development database. Passing an argument will connect you to a different 151 | database, like rails dbconsole production. Currently works for MySQL, 152 | PostgreSQL and SQLite 3. 153 | 154 | == Description of Contents 155 | 156 | The default directory structure of a generated Ruby on Rails application: 157 | 158 | |-- app 159 | | |-- assets 160 | | |-- images 161 | | |-- javascripts 162 | | `-- stylesheets 163 | | |-- controllers 164 | | |-- helpers 165 | | |-- mailers 166 | | |-- models 167 | | `-- views 168 | | `-- layouts 169 | |-- config 170 | | |-- environments 171 | | |-- initializers 172 | | `-- locales 173 | |-- db 174 | |-- doc 175 | |-- lib 176 | | `-- tasks 177 | |-- log 178 | |-- public 179 | |-- script 180 | |-- test 181 | | |-- fixtures 182 | | |-- functional 183 | | |-- integration 184 | | |-- performance 185 | | `-- unit 186 | |-- tmp 187 | | |-- cache 188 | | |-- pids 189 | | |-- sessions 190 | | `-- sockets 191 | `-- vendor 192 | |-- assets 193 | `-- stylesheets 194 | `-- plugins 195 | 196 | app 197 | Holds all the code that's specific to this particular application. 198 | 199 | app/assets 200 | Contains subdirectories for images, stylesheets, and JavaScript files. 201 | 202 | app/controllers 203 | Holds controllers that should be named like weblogs_controller.rb for 204 | automated URL mapping. All controllers should descend from 205 | ApplicationController which itself descends from ActionController::Base. 206 | 207 | app/models 208 | Holds models that should be named like post.rb. Models descend from 209 | ActiveRecord::Base by default. 210 | 211 | app/views 212 | Holds the template files for the view that should be named like 213 | weblogs/index.html.erb for the WeblogsController#index action. All views use 214 | eRuby syntax by default. 215 | 216 | app/views/layouts 217 | Holds the template files for layouts to be used with views. This models the 218 | common header/footer method of wrapping views. In your views, define a layout 219 | using the layout :default and create a file named default.html.erb. 220 | Inside default.html.erb, call <% yield %> to render the view using this 221 | layout. 222 | 223 | app/helpers 224 | Holds view helpers that should be named like weblogs_helper.rb. These are 225 | generated for you automatically when using generators for controllers. 226 | Helpers can be used to wrap functionality for your views into methods. 227 | 228 | config 229 | Configuration files for the Rails environment, the routing map, the database, 230 | and other dependencies. 231 | 232 | db 233 | Contains the database schema in schema.rb. db/migrate contains all the 234 | sequence of Migrations for your schema. 235 | 236 | doc 237 | This directory is where your application documentation will be stored when 238 | generated using rake doc:app 239 | 240 | lib 241 | Application specific libraries. Basically, any kind of custom code that 242 | doesn't belong under controllers, models, or helpers. This directory is in 243 | the load path. 244 | 245 | public 246 | The directory available for the web server. Also contains the dispatchers and the 247 | default HTML files. This should be set as the DOCUMENT_ROOT of your web 248 | server. 249 | 250 | script 251 | Helper scripts for automation and generation. 252 | 253 | test 254 | Unit and functional tests along with fixtures. When using the rails generate 255 | command, template test files will be generated for you and placed in this 256 | directory. 257 | 258 | vendor 259 | External libraries that the application depends on. Also includes the plugins 260 | subdirectory. If the app has frozen rails, those gems also go here, under 261 | vendor/rails/. This directory is in the load path. 262 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/test/dummy/app/mailers/.gitkeep -------------------------------------------------------------------------------- /test/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/test/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application", :media => "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require 6 | require "backbone-on-rails" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Custom directories with classes and modules you want to be autoloadable. 15 | # config.autoload_paths += %W(#{config.root}/extras) 16 | 17 | # Only load the plugins named here, in the order given (default is alphabetical). 18 | # :all can be used as a placeholder for all plugins not explicitly named. 19 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 20 | 21 | # Activate observers that should always be running. 22 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 23 | 24 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 25 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 26 | # config.time_zone = 'Central Time (US & Canada)' 27 | 28 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 29 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 30 | # config.i18n.default_locale = :de 31 | 32 | # Configure the default encoding used in templates for Ruby 1.9. 33 | config.encoding = "utf-8" 34 | 35 | # Configure sensitive parameters which will be filtered from the log file. 36 | config.filter_parameters += [:password] 37 | 38 | # Enable escaping HTML in JSON. 39 | config.active_support.escape_html_entities_in_json = true 40 | 41 | # Use SQL instead of Active Record's schema dumper when creating the database. 42 | # This is necessary if your schema can't be completely dumped by the schema dumper, 43 | # like if you have constraints or database-specific column types 44 | # config.active_record.schema_format = :sql 45 | 46 | # Enforce whitelist mode for mass assignment. 47 | # This will create an empty whitelist of attributes available for mass-assignment for all models 48 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 49 | # parameters by using an attr_accessible or attr_protected declaration. 50 | config.active_record.whitelist_attributes = true 51 | 52 | # Enable the asset pipeline 53 | config.assets.enabled = true 54 | 55 | # Version of your assets, change this if you want to expire all your assets 56 | config.assets.version = '1.0' 57 | end 58 | end 59 | 60 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise exception on mass assignment protection for Active Record models 26 | config.active_record.mass_assignment_sanitizer = :strict 27 | 28 | # Log the query plan for queries taking more than this (works 29 | # with SQLite, MySQL, and PostgreSQL) 30 | config.active_record.auto_explain_threshold_in_seconds = 0.5 31 | 32 | # Do not compress assets 33 | config.assets.compress = false 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = true 37 | end 38 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Raise exception on mass assignment protection for Active Record models 33 | config.active_record.mass_assignment_sanitizer = :strict 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | end 38 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = 'dbaf54ff59679a5fdbd306e9919b683a8e297fcb194cc26f348a514b38a56d534b8e038a938f63d61f29bb162b56720dc3de0bd0eb3133ea6737aa2ffee662a3' 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | # The priority is based upon order of creation: 3 | # first created -> highest priority. 4 | 5 | # Sample of regular route: 6 | # match 'products/:id' => 'catalog#view' 7 | # Keep in mind you can assign values other than :controller and :action 8 | 9 | # Sample of named route: 10 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 11 | # This route can be invoked with purchase_url(:id => product.id) 12 | 13 | # Sample resource route (maps HTTP verbs to controller actions automatically): 14 | # resources :products 15 | 16 | # Sample resource route with options: 17 | # resources :products do 18 | # member do 19 | # get 'short' 20 | # post 'toggle' 21 | # end 22 | # 23 | # collection do 24 | # get 'sold' 25 | # end 26 | # end 27 | 28 | # Sample resource route with sub-resources: 29 | # resources :products do 30 | # resources :comments, :sales 31 | # resource :seller 32 | # end 33 | 34 | # Sample resource route with more complex sub-resources 35 | # resources :products do 36 | # resources :comments 37 | # resources :sales do 38 | # get 'recent', :on => :collection 39 | # end 40 | # end 41 | 42 | # Sample resource route within a namespace: 43 | # namespace :admin do 44 | # # Directs /admin/products/* to Admin::ProductsController 45 | # # (app/controllers/admin/products_controller.rb) 46 | # resources :products 47 | # end 48 | 49 | # You can have the root of your site routed with "root" 50 | # just remember to delete public/index.html. 51 | # root :to => 'welcome#index' 52 | 53 | # See how all your routes lay out with "rake routes" 54 | 55 | # This is a legacy wild controller route that's not recommended for RESTful applications. 56 | # Note: This route will make all actions in every controller accessible via GET requests. 57 | # match ':controller(/:action(/:id))(.:format)' 58 | end 59 | -------------------------------------------------------------------------------- /test/dummy/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/test/dummy/db/test.sqlite3 -------------------------------------------------------------------------------- /test/dummy/lib/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/test/dummy/lib/assets/.gitkeep -------------------------------------------------------------------------------- /test/dummy/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/test/dummy/log/.gitkeep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meleyal/backbone-on-rails/8952da16397913f6a73a66a2e7accebde22f6eac/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /test/generators/install_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'generators/backbone/helpers' 3 | require 'generators/backbone/install/install_generator' 4 | 5 | class InstallGeneratorTest < Rails::Generators::TestCase 6 | 7 | tests Backbone::Generators::InstallGenerator 8 | 9 | # Include custom helpers 10 | include Backbone::Generators::Helpers 11 | 12 | # Generator output directory ('Rails.root') 13 | destination File.expand_path("../../tmp", __FILE__) 14 | 15 | # Erase destination directory 16 | setup :prepare_destination 17 | 18 | test "directory structure is created" do 19 | run_generator 20 | assert_directory model_path 21 | assert_directory collection_path 22 | assert_directory router_path 23 | assert_directory view_path 24 | assert_directory template_path 25 | end 26 | 27 | test "directory structure is created in a subdirectory" do 28 | run_generator ['-d=custom'] 29 | assert_directory 'app/assets/javascripts/custom/routers' 30 | assert_directory 'app/assets/javascripts/custom/models' 31 | assert_directory 'app/assets/javascripts/custom/collections' 32 | assert_directory 'app/assets/javascripts/custom/views' 33 | assert_directory 'app/assets/templates/custom' 34 | end 35 | 36 | test "app coffee file is created" do 37 | run_generator 38 | assert_file "#{javascript_path}/#{app_filename}.js.coffee" do |content| 39 | assert_match(/window\.#{app_name}/, content) 40 | assert_match(/#{app_name}\.initialize/, content) 41 | end 42 | end 43 | 44 | test "app coffee file is created with custom app name" do 45 | run_generator ['-a=Custom'] 46 | assert_file "#{javascript_path}/custom.js.coffee" do |content| 47 | assert_match(/window\.Custom/, content) 48 | assert_match(/Custom\.initialize/, content) 49 | end 50 | end 51 | 52 | test "app javascript file is created" do 53 | run_generator ['--javascript'] 54 | assert_file "#{javascript_path}/#{app_filename}.js" do |content| 55 | assert_match(/window\.#{app_name}/, content) 56 | assert_match(/#{app_name}\.initialize/, content) 57 | end 58 | end 59 | 60 | test "require paths are injected into default manifest" do 61 | copy_manifest 62 | run_generator 63 | assert_file "#{javascript_path}/application.js" do |content| 64 | test_manifest_content(content) 65 | end 66 | end 67 | 68 | test "require paths are injected into empty manifest" do 69 | create_empty_manifest 70 | run_generator 71 | assert_file "#{javascript_path}/application.js" do |content| 72 | test_manifest_content(content) 73 | end 74 | end 75 | 76 | test "require paths are injected into custom manifest" do 77 | manifest = 'index.js' 78 | run_generator ['--manifest', manifest] 79 | assert_file "#{javascript_path}/#{manifest}" do |content| 80 | test_manifest_content(content) 81 | end 82 | end 83 | 84 | private 85 | 86 | def test_manifest_content(content) 87 | assert_match(/require underscore\n/, content) 88 | assert_match(/require backbone\n/, content) 89 | assert_match(/require #{app_filename}\n/, content) 90 | assert_match(/require_tree \.\.\/templates\n/, content) 91 | assert_match(/require_tree \.\/models\n/, content) 92 | assert_match(/require_tree \.\/collections\n/, content) 93 | assert_match(/require_tree \.\/views\n/, content) 94 | assert_match(/require_tree \.\/routers\n/, content) 95 | end 96 | 97 | def copy_manifest 98 | manifest = File.expand_path("../../dummy/app/assets/javascripts/application.js", __FILE__) 99 | destination = File.join destination_root, javascript_path 100 | FileUtils.mkdir_p destination 101 | FileUtils.cp manifest, destination 102 | end 103 | 104 | def create_empty_manifest 105 | destination = File.join destination_root, javascript_path 106 | manifest = File.join destination, 'application.js' 107 | FileUtils.mkdir_p destination 108 | FileUtils.touch manifest 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /test/generators/scaffold_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'generators/backbone/helpers' 3 | require "generators/backbone/scaffold/scaffold_generator" 4 | 5 | class ScaffoldGeneratorTest < Rails::Generators::TestCase 6 | 7 | tests Backbone::Generators::ScaffoldGenerator 8 | 9 | # Include custom helpers 10 | include Backbone::Generators::Helpers 11 | 12 | # Generator output directory ('Rails.root') 13 | destination File.expand_path("../../tmp", __FILE__) 14 | 15 | # Erase destination directory 16 | setup :prepare_destination 17 | 18 | test "model coffee file is created" do 19 | run_generator ['planet'] 20 | assert_file "#{model_path}/planet.js.coffee" do |content| 21 | assert_match('class Dummy.Models.Planet', content) 22 | end 23 | end 24 | 25 | test "model coffee file is created with custom Appname" do 26 | run_generator ['planet', '-a=Custom'] 27 | assert_file "#{model_path}/planet.js.coffee" do |content| 28 | assert_match('class Custom.Models.Planet', content) 29 | end 30 | end 31 | 32 | test "model coffee file is created in a subdir" do 33 | run_generator ['planet', '--dir=custom'] 34 | assert_file "app/assets/javascripts/custom/models/planet.js.coffee" do |content| 35 | assert_match('class Dummy.Models.Planet', content) 36 | end 37 | end 38 | 39 | test "model javascript file is created" do 40 | run_generator ['planet', '--javascript'] 41 | assert_file "#{model_path}/planet.js" do |content| 42 | assert_match('Dummy.Models.Planet = Backbone.Model.extend', content) 43 | end 44 | end 45 | 46 | test "collection coffee file is created" do 47 | run_generator ['planet'] 48 | assert_file "#{collection_path}/planets.js.coffee" do |content| 49 | assert_match('class Dummy.Collections.Planets', content) 50 | assert_match('model: Dummy.Models.Planet', content) 51 | end 52 | end 53 | 54 | test "collection coffee file is created with custom App" do 55 | run_generator ['planet', '-a=Custom'] 56 | assert_file "#{collection_path}/planets.js.coffee" do |content| 57 | assert_match('class Custom.Collections.Planets', content) 58 | assert_match('model: Custom.Models.Planet', content) 59 | end 60 | end 61 | 62 | test "collection coffee file is created in a subdir" do 63 | run_generator ['planet', '--dir=custom', '--app=Custom'] 64 | assert_file "app/assets/javascripts/custom/collections/planets.js.coffee" do |content| 65 | assert_match('class Custom.Collections.Planets', content) 66 | assert_match('model: Custom.Models.Planet', content) 67 | end 68 | end 69 | 70 | test "collection javascript file is created" do 71 | run_generator ['planet', '--javascript'] 72 | assert_file "#{collection_path}/planets.js" do |content| 73 | assert_match('Dummy.Collections.Planets = Backbone.Collection.extend', content) 74 | assert_match('model: Dummy.Models.Planet', content) 75 | end 76 | end 77 | 78 | test "router coffee file is created" do 79 | run_generator ['planet'] 80 | assert_file "#{router_path}/planets_router.js.coffee" do |content| 81 | assert_match('class Dummy.Routers.Planets', content) 82 | end 83 | end 84 | 85 | test "router javascript file is created" do 86 | run_generator ['planet', '--javascript'] 87 | assert_file "#{router_path}/planets_router.js" do |content| 88 | assert_match('Dummy.Routers.Planets = Backbone.Router.extend', content) 89 | end 90 | end 91 | 92 | test "router coffee file is created in a subdir" do 93 | run_generator ['planet', '--dir=custom'] 94 | assert_file "app/assets/javascripts/custom/routers/planets_router.js.coffee" do |content| 95 | assert_match('class Dummy.Routers.Planets', content) 96 | end 97 | end 98 | 99 | test "router coffee file is created with custom appname" do 100 | run_generator ['planet', '-a=Custom'] 101 | assert_file "#{router_path}/planets_router.js.coffee" do |content| 102 | assert_match('class Custom.Routers.Planets', content) 103 | end 104 | end 105 | 106 | test "view coffee file is created" do 107 | run_generator ['planet'] 108 | assert_file "#{view_path}/planets/planets_index.js.coffee" do |content| 109 | assert_match('class Dummy.Views.PlanetsIndex', content) 110 | assert_match("template: JST['planets/index']", content) 111 | end 112 | end 113 | 114 | test "view coffee file is created in subdir" do 115 | run_generator ['planet', '-d=custom'] 116 | assert_file "app/assets/javascripts/custom/views/planets/planets_index.js.coffee" do |content| 117 | assert_match('class Dummy.Views.PlanetsIndex', content) 118 | assert_match("template: JST['custom/planets/index']", content) 119 | end 120 | end 121 | 122 | test "view javascript file is created" do 123 | run_generator ['planet', '--javascript'] 124 | assert_file "#{view_path}/planets/planets_index.js" do |content| 125 | assert_match('Dummy.Views.PlanetsIndex = Backbone.View.extend', content) 126 | assert_match("template: JST['planets/index']", content) 127 | end 128 | end 129 | 130 | test "view coffee file is created with a different template controller" do 131 | run_generator ['planet', '--template_namespace=SHT'] 132 | assert_file "#{view_path}/planets/planets_index.js.coffee" do |content| 133 | assert_match('class Dummy.Views.PlanetsIndex', content) 134 | assert_match("template: SHT['planets/index']", content) 135 | end 136 | end 137 | 138 | test "view javascript file is created with a different template controller" do 139 | run_generator ['planet', '--javascript', '--template_namespace=SHT'] 140 | assert_file "#{view_path}/planets/planets_index.js" do |content| 141 | assert_match('Dummy.Views.PlanetsIndex = Backbone.View.extend', content) 142 | assert_match("template: SHT['planets/index']", content) 143 | end 144 | end 145 | 146 | test "view coffee file is created with custom appname" do 147 | run_generator ['planet', '-a=Custom'] 148 | assert_file "#{view_path}/planets/planets_index.js.coffee" do |content| 149 | assert_match('class Custom.Views.PlanetsIndex', content) 150 | assert_match("template: JST['planets/index']", content) 151 | end 152 | end 153 | 154 | test "template eco file is created with handlebars" do 155 | run_generator ['planet', '--template=hbs'] 156 | assert_file "#{template_path}/planets/index.hbs" 157 | end 158 | 159 | test "template eco file is created" do 160 | run_generator ['planet'] 161 | assert_file "#{template_path}/planets/index.jst.eco" 162 | end 163 | 164 | test "template eco file is created in subdir" do 165 | run_generator ['planet', '-d=custom'] 166 | assert_file "app/assets/templates/custom/planets/index.jst.eco" 167 | end 168 | 169 | test "template jst file is created" do 170 | run_generator ['planet', '--javascript'] 171 | assert_file "#{template_path}/planets/index.jst.ejs" 172 | end 173 | 174 | end 175 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 5 | require "rails/test_help" 6 | 7 | Rails.backtrace_cleaner.remove_silencers! 8 | 9 | # Load support files 10 | #Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 11 | 12 | # For Generators 13 | require 'rails/generators/test_case' 14 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 1.2.0 2 | 3 | // (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | (function(factory) { 9 | 10 | // Establish the root object, `window` (`self`) in the browser, or `global` on the server. 11 | // We use `self` instead of `window` for `WebWorker` support. 12 | var root = (typeof self == 'object' && self.self == self && self) || 13 | (typeof global == 'object' && global.global == global && global); 14 | 15 | // Set up Backbone appropriately for the environment. Start with AMD. 16 | if (typeof define === 'function' && define.amd) { 17 | define(['underscore', 'jquery', 'exports'], function(_, $, exports) { 18 | // Export global even in AMD case in case this script is loaded with 19 | // others that may still expect a global Backbone. 20 | root.Backbone = factory(root, exports, _, $); 21 | }); 22 | 23 | // Next for Node.js or CommonJS. jQuery may not be needed as a module. 24 | } else if (typeof exports !== 'undefined') { 25 | var _ = require('underscore'), $; 26 | try { $ = require('jquery'); } catch(e) {} 27 | factory(root, exports, _, $); 28 | 29 | // Finally, as a browser global. 30 | } else { 31 | root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); 32 | } 33 | 34 | }(function(root, Backbone, _, $) { 35 | 36 | // Initial Setup 37 | // ------------- 38 | 39 | // Save the previous value of the `Backbone` variable, so that it can be 40 | // restored later on, if `noConflict` is used. 41 | var previousBackbone = root.Backbone; 42 | 43 | // Create local references to array methods we'll want to use later. 44 | var array = []; 45 | var slice = array.slice; 46 | 47 | // Current version of the library. Keep in sync with `package.json`. 48 | Backbone.VERSION = '1.2.0'; 49 | 50 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 51 | // the `$` variable. 52 | Backbone.$ = $; 53 | 54 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 55 | // to its previous owner. Returns a reference to this Backbone object. 56 | Backbone.noConflict = function() { 57 | root.Backbone = previousBackbone; 58 | return this; 59 | }; 60 | 61 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 62 | // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and 63 | // set a `X-Http-Method-Override` header. 64 | Backbone.emulateHTTP = false; 65 | 66 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 67 | // `application/json` requests ... this will encode the body as 68 | // `application/x-www-form-urlencoded` instead and will send the model in a 69 | // form param named `model`. 70 | Backbone.emulateJSON = false; 71 | 72 | // Backbone.Events 73 | // --------------- 74 | 75 | // A module that can be mixed in to *any object* in order to provide it with 76 | // custom events. You may bind with `on` or remove with `off` callback 77 | // functions to an event; `trigger`-ing an event fires all callbacks in 78 | // succession. 79 | // 80 | // var object = {}; 81 | // _.extend(object, Backbone.Events); 82 | // object.on('expand', function(){ alert('expanded'); }); 83 | // object.trigger('expand'); 84 | // 85 | var Events = Backbone.Events = {}; 86 | 87 | // Regular expression used to split event strings. 88 | var eventSplitter = /\s+/; 89 | 90 | // Iterates over the standard `event, callback` (as well as the fancy multiple 91 | // space-separated events `"change blur", callback` and jQuery-style event 92 | // maps `{event: callback}`), reducing them by manipulating `memo`. 93 | // Passes a normalized single event name and callback, as well as any 94 | // optional `opts`. 95 | var eventsApi = function(iteratee, memo, name, callback, opts) { 96 | var i = 0, names; 97 | if (name && typeof name === 'object') { 98 | // Handle event maps. 99 | for (names = _.keys(name); i < names.length ; i++) { 100 | memo = iteratee(memo, names[i], name[names[i]], opts); 101 | } 102 | } else if (name && eventSplitter.test(name)) { 103 | // Handle space separated event names. 104 | for (names = name.split(eventSplitter); i < names.length; i++) { 105 | memo = iteratee(memo, names[i], callback, opts); 106 | } 107 | } else { 108 | memo = iteratee(memo, name, callback, opts); 109 | } 110 | return memo; 111 | }; 112 | 113 | // Bind an event to a `callback` function. Passing `"all"` will bind 114 | // the callback to all events fired. 115 | Events.on = function(name, callback, context) { 116 | return internalOn(this, name, callback, context); 117 | }; 118 | 119 | // An internal use `on` function, used to guard the `listening` argument from 120 | // the public API. 121 | var internalOn = function(obj, name, callback, context, listening) { 122 | obj._events = eventsApi(onApi, obj._events || {}, name, callback, { 123 | context: context, 124 | ctx: obj, 125 | listening: listening 126 | }); 127 | 128 | if (listening) { 129 | var listeners = obj._listeners || (obj._listeners = {}); 130 | listeners[listening.id] = listening; 131 | } 132 | 133 | return obj; 134 | }; 135 | 136 | // Inversion-of-control versions of `on`. Tell *this* object to listen to 137 | // an event in another object... keeping track of what it's listening to. 138 | Events.listenTo = function(obj, name, callback) { 139 | if (!obj) return this; 140 | var id = obj._listenId || (obj._listenId = _.uniqueId('l')); 141 | var listeningTo = this._listeningTo || (this._listeningTo = {}); 142 | var listening = listeningTo[id]; 143 | 144 | // This object is not listening to any other events on `obj` yet. 145 | // Setup the necessary references to track the listening callbacks. 146 | if (!listening) { 147 | var thisId = this._listenId || (this._listenId = _.uniqueId('l')); 148 | listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; 149 | } 150 | 151 | // Bind callbacks on obj, and keep track of them on listening. 152 | internalOn(obj, name, callback, this, listening); 153 | return this; 154 | }; 155 | 156 | // The reducing API that adds a callback to the `events` object. 157 | var onApi = function(events, name, callback, options) { 158 | if (callback) { 159 | var handlers = events[name] || (events[name] = []); 160 | var context = options.context, ctx = options.ctx, listening = options.listening; 161 | if (listening) listening.count++; 162 | 163 | handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); 164 | } 165 | return events; 166 | }; 167 | 168 | // Remove one or many callbacks. If `context` is null, removes all 169 | // callbacks with that function. If `callback` is null, removes all 170 | // callbacks for the event. If `name` is null, removes all bound 171 | // callbacks for all events. 172 | Events.off = function(name, callback, context) { 173 | if (!this._events) return this; 174 | this._events = eventsApi(offApi, this._events, name, callback, { 175 | context: context, 176 | listeners: this._listeners 177 | }); 178 | return this; 179 | }; 180 | 181 | // Tell this object to stop listening to either specific events ... or 182 | // to every object it's currently listening to. 183 | Events.stopListening = function(obj, name, callback) { 184 | var listeningTo = this._listeningTo; 185 | if (!listeningTo) return this; 186 | 187 | var ids = obj ? [obj._listenId] : _.keys(listeningTo); 188 | 189 | for (var i = 0; i < ids.length; i++) { 190 | var listening = listeningTo[ids[i]]; 191 | 192 | // If listening doesn't exist, this object is not currently 193 | // listening to obj. Break out early. 194 | if (!listening) break; 195 | 196 | listening.obj.off(name, callback, this); 197 | } 198 | if (_.isEmpty(listeningTo)) this._listeningTo = void 0; 199 | 200 | return this; 201 | }; 202 | 203 | // The reducing API that removes a callback from the `events` object. 204 | var offApi = function(events, name, callback, options) { 205 | // No events to consider. 206 | if (!events) return; 207 | 208 | var i = 0, length, listening; 209 | var context = options.context, listeners = options.listeners; 210 | 211 | // Delete all events listeners and "drop" events. 212 | if (!name && !callback && !context) { 213 | var ids = _.keys(listeners); 214 | for (; i < ids.length; i++) { 215 | listening = listeners[ids[i]]; 216 | delete listeners[listening.id]; 217 | delete listening.listeningTo[listening.objId]; 218 | } 219 | return; 220 | } 221 | 222 | var names = name ? [name] : _.keys(events); 223 | for (; i < names.length; i++) { 224 | name = names[i]; 225 | var handlers = events[name]; 226 | 227 | // Bail out if there are no events stored. 228 | if (!handlers) break; 229 | 230 | // Replace events if there are any remaining. Otherwise, clean up. 231 | var remaining = []; 232 | for (var j = 0; j < handlers.length; j++) { 233 | var handler = handlers[j]; 234 | if ( 235 | callback && callback !== handler.callback && 236 | callback !== handler.callback._callback || 237 | context && context !== handler.context 238 | ) { 239 | remaining.push(handler); 240 | } else { 241 | listening = handler.listening; 242 | if (listening && --listening.count === 0) { 243 | delete listeners[listening.id]; 244 | delete listening.listeningTo[listening.objId]; 245 | } 246 | } 247 | } 248 | 249 | // Update tail event if the list has any events. Otherwise, clean up. 250 | if (remaining.length) { 251 | events[name] = remaining; 252 | } else { 253 | delete events[name]; 254 | } 255 | } 256 | if (_.size(events)) return events; 257 | }; 258 | 259 | // Bind an event to only be triggered a single time. After the first time 260 | // the callback is invoked, it will be removed. When multiple events are 261 | // passed in using the space-separated syntax, the event will fire once for every 262 | // event you passed in, not once for a combination of all events 263 | Events.once = function(name, callback, context) { 264 | // Map the event into a `{event: once}` object. 265 | var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); 266 | return this.on(events, void 0, context); 267 | }; 268 | 269 | // Inversion-of-control versions of `once`. 270 | Events.listenToOnce = function(obj, name, callback) { 271 | // Map the event into a `{event: once}` object. 272 | var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); 273 | return this.listenTo(obj, events); 274 | }; 275 | 276 | // Reduces the event callbacks into a map of `{event: onceWrapper}`. 277 | // `offer` unbinds the `onceWrapper` after it as been called. 278 | var onceMap = function(map, name, callback, offer) { 279 | if (callback) { 280 | var once = map[name] = _.once(function() { 281 | offer(name, once); 282 | callback.apply(this, arguments); 283 | }); 284 | once._callback = callback; 285 | } 286 | return map; 287 | }; 288 | 289 | // Trigger one or many events, firing all bound callbacks. Callbacks are 290 | // passed the same arguments as `trigger` is, apart from the event name 291 | // (unless you're listening on `"all"`, which will cause your callback to 292 | // receive the true name of the event as the first argument). 293 | Events.trigger = function(name) { 294 | if (!this._events) return this; 295 | 296 | var length = Math.max(0, arguments.length - 1); 297 | var args = Array(length); 298 | for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; 299 | 300 | eventsApi(triggerApi, this._events, name, void 0, args); 301 | return this; 302 | }; 303 | 304 | // Handles triggering the appropriate event callbacks. 305 | var triggerApi = function(objEvents, name, cb, args) { 306 | if (objEvents) { 307 | var events = objEvents[name]; 308 | var allEvents = objEvents.all; 309 | if (events && allEvents) allEvents = allEvents.slice(); 310 | if (events) triggerEvents(events, args); 311 | if (allEvents) triggerEvents(allEvents, [name].concat(args)); 312 | } 313 | return objEvents; 314 | }; 315 | 316 | // A difficult-to-believe, but optimized internal dispatch function for 317 | // triggering events. Tries to keep the usual cases speedy (most internal 318 | // Backbone events have 3 arguments). 319 | var triggerEvents = function(events, args) { 320 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 321 | switch (args.length) { 322 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 323 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 324 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 325 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 326 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; 327 | } 328 | }; 329 | 330 | // Proxy Underscore methods to a Backbone class' prototype using a 331 | // particular attribute as the data argument 332 | var addMethod = function(length, method, attribute) { 333 | switch (length) { 334 | case 1: return function() { 335 | return _[method](this[attribute]); 336 | }; 337 | case 2: return function(value) { 338 | return _[method](this[attribute], value); 339 | }; 340 | case 3: return function(iteratee, context) { 341 | return _[method](this[attribute], iteratee, context); 342 | }; 343 | case 4: return function(iteratee, defaultVal, context) { 344 | return _[method](this[attribute], iteratee, defaultVal, context); 345 | }; 346 | default: return function() { 347 | var args = slice.call(arguments); 348 | args.unshift(this[attribute]); 349 | return _[method].apply(_, args); 350 | }; 351 | } 352 | }; 353 | var addUnderscoreMethods = function(Class, methods, attribute) { 354 | _.each(methods, function(length, method) { 355 | if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); 356 | }); 357 | }; 358 | 359 | // Aliases for backwards compatibility. 360 | Events.bind = Events.on; 361 | Events.unbind = Events.off; 362 | 363 | // Allow the `Backbone` object to serve as a global event bus, for folks who 364 | // want global "pubsub" in a convenient place. 365 | _.extend(Backbone, Events); 366 | 367 | // Backbone.Model 368 | // -------------- 369 | 370 | // Backbone **Models** are the basic data object in the framework -- 371 | // frequently representing a row in a table in a database on your server. 372 | // A discrete chunk of data and a bunch of useful, related methods for 373 | // performing computations and transformations on that data. 374 | 375 | // Create a new model with the specified attributes. A client id (`cid`) 376 | // is automatically generated and assigned for you. 377 | var Model = Backbone.Model = function(attributes, options) { 378 | var attrs = attributes || {}; 379 | options || (options = {}); 380 | this.cid = _.uniqueId(this.cidPrefix); 381 | this.attributes = {}; 382 | if (options.collection) this.collection = options.collection; 383 | if (options.parse) attrs = this.parse(attrs, options) || {}; 384 | attrs = _.defaults({}, attrs, _.result(this, 'defaults')); 385 | this.set(attrs, options); 386 | this.changed = {}; 387 | this.initialize.apply(this, arguments); 388 | }; 389 | 390 | // Attach all inheritable methods to the Model prototype. 391 | _.extend(Model.prototype, Events, { 392 | 393 | // A hash of attributes whose current and previous value differ. 394 | changed: null, 395 | 396 | // The value returned during the last failed validation. 397 | validationError: null, 398 | 399 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 400 | // CouchDB users may want to set this to `"_id"`. 401 | idAttribute: 'id', 402 | 403 | // The prefix is used to create the client id which is used to identify models locally. 404 | // You may want to override this if you're experiencing name clashes with model ids. 405 | cidPrefix: 'c', 406 | 407 | // Initialize is an empty function by default. Override it with your own 408 | // initialization logic. 409 | initialize: function(){}, 410 | 411 | // Return a copy of the model's `attributes` object. 412 | toJSON: function(options) { 413 | return _.clone(this.attributes); 414 | }, 415 | 416 | // Proxy `Backbone.sync` by default -- but override this if you need 417 | // custom syncing semantics for *this* particular model. 418 | sync: function() { 419 | return Backbone.sync.apply(this, arguments); 420 | }, 421 | 422 | // Get the value of an attribute. 423 | get: function(attr) { 424 | return this.attributes[attr]; 425 | }, 426 | 427 | // Get the HTML-escaped value of an attribute. 428 | escape: function(attr) { 429 | return _.escape(this.get(attr)); 430 | }, 431 | 432 | // Returns `true` if the attribute contains a value that is not null 433 | // or undefined. 434 | has: function(attr) { 435 | return this.get(attr) != null; 436 | }, 437 | 438 | // Special-cased proxy to underscore's `_.matches` method. 439 | matches: function(attrs) { 440 | return !!_.iteratee(attrs, this)(this.attributes); 441 | }, 442 | 443 | // Set a hash of model attributes on the object, firing `"change"`. This is 444 | // the core primitive operation of a model, updating the data and notifying 445 | // anyone who needs to know about the change in state. The heart of the beast. 446 | set: function(key, val, options) { 447 | var attr, attrs, unset, changes, silent, changing, prev, current; 448 | if (key == null) return this; 449 | 450 | // Handle both `"key", value` and `{key: value}` -style arguments. 451 | if (typeof key === 'object') { 452 | attrs = key; 453 | options = val; 454 | } else { 455 | (attrs = {})[key] = val; 456 | } 457 | 458 | options || (options = {}); 459 | 460 | // Run validation. 461 | if (!this._validate(attrs, options)) return false; 462 | 463 | // Extract attributes and options. 464 | unset = options.unset; 465 | silent = options.silent; 466 | changes = []; 467 | changing = this._changing; 468 | this._changing = true; 469 | 470 | if (!changing) { 471 | this._previousAttributes = _.clone(this.attributes); 472 | this.changed = {}; 473 | } 474 | current = this.attributes, prev = this._previousAttributes; 475 | 476 | // Check for changes of `id`. 477 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 478 | 479 | // For each `set` attribute, update or delete the current value. 480 | for (attr in attrs) { 481 | val = attrs[attr]; 482 | if (!_.isEqual(current[attr], val)) changes.push(attr); 483 | if (!_.isEqual(prev[attr], val)) { 484 | this.changed[attr] = val; 485 | } else { 486 | delete this.changed[attr]; 487 | } 488 | unset ? delete current[attr] : current[attr] = val; 489 | } 490 | 491 | // Trigger all relevant attribute changes. 492 | if (!silent) { 493 | if (changes.length) this._pending = options; 494 | for (var i = 0; i < changes.length; i++) { 495 | this.trigger('change:' + changes[i], this, current[changes[i]], options); 496 | } 497 | } 498 | 499 | // You might be wondering why there's a `while` loop here. Changes can 500 | // be recursively nested within `"change"` events. 501 | if (changing) return this; 502 | if (!silent) { 503 | while (this._pending) { 504 | options = this._pending; 505 | this._pending = false; 506 | this.trigger('change', this, options); 507 | } 508 | } 509 | this._pending = false; 510 | this._changing = false; 511 | return this; 512 | }, 513 | 514 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop 515 | // if the attribute doesn't exist. 516 | unset: function(attr, options) { 517 | return this.set(attr, void 0, _.extend({}, options, {unset: true})); 518 | }, 519 | 520 | // Clear all attributes on the model, firing `"change"`. 521 | clear: function(options) { 522 | var attrs = {}; 523 | for (var key in this.attributes) attrs[key] = void 0; 524 | return this.set(attrs, _.extend({}, options, {unset: true})); 525 | }, 526 | 527 | // Determine if the model has changed since the last `"change"` event. 528 | // If you specify an attribute name, determine if that attribute has changed. 529 | hasChanged: function(attr) { 530 | if (attr == null) return !_.isEmpty(this.changed); 531 | return _.has(this.changed, attr); 532 | }, 533 | 534 | // Return an object containing all the attributes that have changed, or 535 | // false if there are no changed attributes. Useful for determining what 536 | // parts of a view need to be updated and/or what attributes need to be 537 | // persisted to the server. Unset attributes will be set to undefined. 538 | // You can also pass an attributes object to diff against the model, 539 | // determining if there *would be* a change. 540 | changedAttributes: function(diff) { 541 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 542 | var val, changed = false; 543 | var old = this._changing ? this._previousAttributes : this.attributes; 544 | for (var attr in diff) { 545 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 546 | (changed || (changed = {}))[attr] = val; 547 | } 548 | return changed; 549 | }, 550 | 551 | // Get the previous value of an attribute, recorded at the time the last 552 | // `"change"` event was fired. 553 | previous: function(attr) { 554 | if (attr == null || !this._previousAttributes) return null; 555 | return this._previousAttributes[attr]; 556 | }, 557 | 558 | // Get all of the attributes of the model at the time of the previous 559 | // `"change"` event. 560 | previousAttributes: function() { 561 | return _.clone(this._previousAttributes); 562 | }, 563 | 564 | // Fetch the model from the server, merging the response with the model's 565 | // local attributes. Any changed attributes will trigger a "change" event. 566 | fetch: function(options) { 567 | options = options ? _.clone(options) : {}; 568 | if (options.parse === void 0) options.parse = true; 569 | var model = this; 570 | var success = options.success; 571 | options.success = function(resp) { 572 | if (!model.set(model.parse(resp, options), options)) return false; 573 | if (success) success.call(options.context, model, resp, options); 574 | model.trigger('sync', model, resp, options); 575 | }; 576 | wrapError(this, options); 577 | return this.sync('read', this, options); 578 | }, 579 | 580 | // Set a hash of model attributes, and sync the model to the server. 581 | // If the server returns an attributes hash that differs, the model's 582 | // state will be `set` again. 583 | save: function(key, val, options) { 584 | var attrs, method, xhr, attributes = this.attributes, wait; 585 | 586 | // Handle both `"key", value` and `{key: value}` -style arguments. 587 | if (key == null || typeof key === 'object') { 588 | attrs = key; 589 | options = val; 590 | } else { 591 | (attrs = {})[key] = val; 592 | } 593 | 594 | options = _.extend({validate: true}, options); 595 | wait = options.wait; 596 | 597 | // If we're not waiting and attributes exist, save acts as 598 | // `set(attr).save(null, opts)` with validation. Otherwise, check if 599 | // the model will be valid when the attributes, if any, are set. 600 | if (attrs && !wait) { 601 | if (!this.set(attrs, options)) return false; 602 | } else { 603 | if (!this._validate(attrs, options)) return false; 604 | } 605 | 606 | // Set temporary attributes if `{wait: true}`. 607 | if (attrs && wait) { 608 | this.attributes = _.extend({}, attributes, attrs); 609 | } 610 | 611 | // After a successful server-side save, the client is (optionally) 612 | // updated with the server-side state. 613 | if (options.parse === void 0) options.parse = true; 614 | var model = this; 615 | var success = options.success; 616 | options.success = function(resp) { 617 | // Ensure attributes are restored during synchronous saves. 618 | model.attributes = attributes; 619 | var serverAttrs = options.parse ? model.parse(resp, options) : resp; 620 | if (wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 621 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 622 | return false; 623 | } 624 | if (success) success.call(options.context, model, resp, options); 625 | model.trigger('sync', model, resp, options); 626 | }; 627 | wrapError(this, options); 628 | 629 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 630 | if (method === 'patch' && !options.attrs) options.attrs = attrs; 631 | xhr = this.sync(method, this, options); 632 | 633 | // Restore attributes. 634 | if (attrs && wait) this.attributes = attributes; 635 | 636 | return xhr; 637 | }, 638 | 639 | // Destroy this model on the server if it was already persisted. 640 | // Optimistically removes the model from its collection, if it has one. 641 | // If `wait: true` is passed, waits for the server to respond before removal. 642 | destroy: function(options) { 643 | options = options ? _.clone(options) : {}; 644 | var model = this; 645 | var success = options.success; 646 | var wait = options.wait; 647 | 648 | var destroy = function() { 649 | model.stopListening(); 650 | model.trigger('destroy', model, model.collection, options); 651 | }; 652 | 653 | options.success = function(resp) { 654 | if (wait) destroy(); 655 | if (success) success.call(options.context, model, resp, options); 656 | if (!model.isNew()) model.trigger('sync', model, resp, options); 657 | }; 658 | 659 | var xhr = false; 660 | if (this.isNew()) { 661 | _.defer(options.success); 662 | } else { 663 | wrapError(this, options); 664 | xhr = this.sync('delete', this, options); 665 | } 666 | if (!wait) destroy(); 667 | return xhr; 668 | }, 669 | 670 | // Default URL for the model's representation on the server -- if you're 671 | // using Backbone's restful methods, override this to change the endpoint 672 | // that will be called. 673 | url: function() { 674 | var base = 675 | _.result(this, 'urlRoot') || 676 | _.result(this.collection, 'url') || 677 | urlError(); 678 | if (this.isNew()) return base; 679 | var id = this.id || this.attributes[this.idAttribute]; 680 | return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(id); 681 | }, 682 | 683 | // **parse** converts a response into the hash of attributes to be `set` on 684 | // the model. The default implementation is just to pass the response along. 685 | parse: function(resp, options) { 686 | return resp; 687 | }, 688 | 689 | // Create a new model with identical attributes to this one. 690 | clone: function() { 691 | return new this.constructor(this.attributes); 692 | }, 693 | 694 | // A model is new if it has never been saved to the server, and lacks an id. 695 | isNew: function() { 696 | return !this.has(this.idAttribute); 697 | }, 698 | 699 | // Check if the model is currently in a valid state. 700 | isValid: function(options) { 701 | return this._validate({}, _.extend(options || {}, { validate: true })); 702 | }, 703 | 704 | // Run validation against the next complete set of model attributes, 705 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 706 | _validate: function(attrs, options) { 707 | if (!options.validate || !this.validate) return true; 708 | attrs = _.extend({}, this.attributes, attrs); 709 | var error = this.validationError = this.validate(attrs, options) || null; 710 | if (!error) return true; 711 | this.trigger('invalid', this, error, _.extend(options, {validationError: error})); 712 | return false; 713 | } 714 | 715 | }); 716 | 717 | // Underscore methods that we want to implement on the Model. 718 | var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, 719 | omit: 0, chain: 1, isEmpty: 1 }; 720 | 721 | // Mix in each Underscore method as a proxy to `Model#attributes`. 722 | addUnderscoreMethods(Model, modelMethods, 'attributes'); 723 | 724 | // Backbone.Collection 725 | // ------------------- 726 | 727 | // If models tend to represent a single row of data, a Backbone Collection is 728 | // more analogous to a table full of data ... or a small slice or page of that 729 | // table, or a collection of rows that belong together for a particular reason 730 | // -- all of the messages in this particular folder, all of the documents 731 | // belonging to this particular author, and so on. Collections maintain 732 | // indexes of their models, both in order, and for lookup by `id`. 733 | 734 | // Create a new **Collection**, perhaps to contain a specific type of `model`. 735 | // If a `comparator` is specified, the Collection will maintain 736 | // its models in sort order, as they're added and removed. 737 | var Collection = Backbone.Collection = function(models, options) { 738 | options || (options = {}); 739 | if (options.model) this.model = options.model; 740 | if (options.comparator !== void 0) this.comparator = options.comparator; 741 | this._reset(); 742 | this.initialize.apply(this, arguments); 743 | if (models) this.reset(models, _.extend({silent: true}, options)); 744 | }; 745 | 746 | // Default options for `Collection#set`. 747 | var setOptions = {add: true, remove: true, merge: true}; 748 | var addOptions = {add: true, remove: false}; 749 | 750 | // Define the Collection's inheritable methods. 751 | _.extend(Collection.prototype, Events, { 752 | 753 | // The default model for a collection is just a **Backbone.Model**. 754 | // This should be overridden in most cases. 755 | model: Model, 756 | 757 | // Initialize is an empty function by default. Override it with your own 758 | // initialization logic. 759 | initialize: function(){}, 760 | 761 | // The JSON representation of a Collection is an array of the 762 | // models' attributes. 763 | toJSON: function(options) { 764 | return this.map(function(model){ return model.toJSON(options); }); 765 | }, 766 | 767 | // Proxy `Backbone.sync` by default. 768 | sync: function() { 769 | return Backbone.sync.apply(this, arguments); 770 | }, 771 | 772 | // Add a model, or list of models to the set. 773 | add: function(models, options) { 774 | return this.set(models, _.extend({merge: false}, options, addOptions)); 775 | }, 776 | 777 | // Remove a model, or a list of models from the set. 778 | remove: function(models, options) { 779 | var singular = !_.isArray(models), removed; 780 | models = singular ? [models] : _.clone(models); 781 | options || (options = {}); 782 | removed = this._removeModels(models, options); 783 | if (!options.silent && removed) this.trigger('update', this, options); 784 | return singular ? models[0] : models; 785 | }, 786 | 787 | // Update a collection by `set`-ing a new list of models, adding new ones, 788 | // removing models that are no longer present, and merging models that 789 | // already exist in the collection, as necessary. Similar to **Model#set**, 790 | // the core operation for updating the data contained by the collection. 791 | set: function(models, options) { 792 | options = _.defaults({}, options, setOptions); 793 | if (options.parse) models = this.parse(models, options); 794 | var singular = !_.isArray(models); 795 | models = singular ? (models ? [models] : []) : models.slice(); 796 | var id, model, attrs, existing, sort; 797 | var at = options.at; 798 | if (at != null) at = +at; 799 | if (at < 0) at += this.length + 1; 800 | var sortable = this.comparator && (at == null) && options.sort !== false; 801 | var sortAttr = _.isString(this.comparator) ? this.comparator : null; 802 | var toAdd = [], toRemove = [], modelMap = {}; 803 | var add = options.add, merge = options.merge, remove = options.remove; 804 | var order = !sortable && add && remove ? [] : false; 805 | var orderChanged = false; 806 | 807 | // Turn bare objects into model references, and prevent invalid models 808 | // from being added. 809 | for (var i = 0; i < models.length; i++) { 810 | attrs = models[i]; 811 | 812 | // If a duplicate is found, prevent it from being added and 813 | // optionally merge it into the existing model. 814 | if (existing = this.get(attrs)) { 815 | if (remove) modelMap[existing.cid] = true; 816 | if (merge && attrs !== existing) { 817 | attrs = this._isModel(attrs) ? attrs.attributes : attrs; 818 | if (options.parse) attrs = existing.parse(attrs, options); 819 | existing.set(attrs, options); 820 | if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 821 | } 822 | models[i] = existing; 823 | 824 | // If this is a new, valid model, push it to the `toAdd` list. 825 | } else if (add) { 826 | model = models[i] = this._prepareModel(attrs, options); 827 | if (!model) continue; 828 | toAdd.push(model); 829 | this._addReference(model, options); 830 | } 831 | 832 | // Do not add multiple models with the same `id`. 833 | model = existing || model; 834 | if (!model) continue; 835 | id = this.modelId(model.attributes); 836 | if (order && (model.isNew() || !modelMap[id])) { 837 | order.push(model); 838 | 839 | // Check to see if this is actually a new model at this index. 840 | orderChanged = orderChanged || !this.models[i] || model.cid !== this.models[i].cid; 841 | } 842 | 843 | modelMap[id] = true; 844 | } 845 | 846 | // Remove nonexistent models if appropriate. 847 | if (remove) { 848 | for (var i = 0; i < this.length; i++) { 849 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 850 | } 851 | if (toRemove.length) this._removeModels(toRemove, options); 852 | } 853 | 854 | // See if sorting is needed, update `length` and splice in new models. 855 | if (toAdd.length || orderChanged) { 856 | if (sortable) sort = true; 857 | this.length += toAdd.length; 858 | if (at != null) { 859 | for (var i = 0; i < toAdd.length; i++) { 860 | this.models.splice(at + i, 0, toAdd[i]); 861 | } 862 | } else { 863 | if (order) this.models.length = 0; 864 | var orderedModels = order || toAdd; 865 | for (var i = 0; i < orderedModels.length; i++) { 866 | this.models.push(orderedModels[i]); 867 | } 868 | } 869 | } 870 | 871 | // Silently sort the collection if appropriate. 872 | if (sort) this.sort({silent: true}); 873 | 874 | // Unless silenced, it's time to fire all appropriate add/sort events. 875 | if (!options.silent) { 876 | var addOpts = at != null ? _.clone(options) : options; 877 | for (var i = 0; i < toAdd.length; i++) { 878 | if (at != null) addOpts.index = at + i; 879 | (model = toAdd[i]).trigger('add', model, this, addOpts); 880 | } 881 | if (sort || orderChanged) this.trigger('sort', this, options); 882 | if (toAdd.length || toRemove.length) this.trigger('update', this, options); 883 | } 884 | 885 | // Return the added (or merged) model (or models). 886 | return singular ? models[0] : models; 887 | }, 888 | 889 | // When you have more items than you want to add or remove individually, 890 | // you can reset the entire set with a new list of models, without firing 891 | // any granular `add` or `remove` events. Fires `reset` when finished. 892 | // Useful for bulk operations and optimizations. 893 | reset: function(models, options) { 894 | options = options ? _.clone(options) : {}; 895 | for (var i = 0; i < this.models.length; i++) { 896 | this._removeReference(this.models[i], options); 897 | } 898 | options.previousModels = this.models; 899 | this._reset(); 900 | models = this.add(models, _.extend({silent: true}, options)); 901 | if (!options.silent) this.trigger('reset', this, options); 902 | return models; 903 | }, 904 | 905 | // Add a model to the end of the collection. 906 | push: function(model, options) { 907 | return this.add(model, _.extend({at: this.length}, options)); 908 | }, 909 | 910 | // Remove a model from the end of the collection. 911 | pop: function(options) { 912 | var model = this.at(this.length - 1); 913 | this.remove(model, options); 914 | return model; 915 | }, 916 | 917 | // Add a model to the beginning of the collection. 918 | unshift: function(model, options) { 919 | return this.add(model, _.extend({at: 0}, options)); 920 | }, 921 | 922 | // Remove a model from the beginning of the collection. 923 | shift: function(options) { 924 | var model = this.at(0); 925 | this.remove(model, options); 926 | return model; 927 | }, 928 | 929 | // Slice out a sub-array of models from the collection. 930 | slice: function() { 931 | return slice.apply(this.models, arguments); 932 | }, 933 | 934 | // Get a model from the set by id. 935 | get: function(obj) { 936 | if (obj == null) return void 0; 937 | var id = this.modelId(this._isModel(obj) ? obj.attributes : obj); 938 | return this._byId[obj] || this._byId[id] || this._byId[obj.cid]; 939 | }, 940 | 941 | // Get the model at the given index. 942 | at: function(index) { 943 | if (index < 0) index += this.length; 944 | return this.models[index]; 945 | }, 946 | 947 | // Return models with matching attributes. Useful for simple cases of 948 | // `filter`. 949 | where: function(attrs, first) { 950 | var matches = _.matches(attrs); 951 | return this[first ? 'find' : 'filter'](function(model) { 952 | return matches(model.attributes); 953 | }); 954 | }, 955 | 956 | // Return the first model with matching attributes. Useful for simple cases 957 | // of `find`. 958 | findWhere: function(attrs) { 959 | return this.where(attrs, true); 960 | }, 961 | 962 | // Force the collection to re-sort itself. You don't need to call this under 963 | // normal circumstances, as the set will maintain sort order as each item 964 | // is added. 965 | sort: function(options) { 966 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 967 | options || (options = {}); 968 | 969 | // Run sort based on type of `comparator`. 970 | if (_.isString(this.comparator) || this.comparator.length === 1) { 971 | this.models = this.sortBy(this.comparator, this); 972 | } else { 973 | this.models.sort(_.bind(this.comparator, this)); 974 | } 975 | 976 | if (!options.silent) this.trigger('sort', this, options); 977 | return this; 978 | }, 979 | 980 | // Pluck an attribute from each model in the collection. 981 | pluck: function(attr) { 982 | return _.invoke(this.models, 'get', attr); 983 | }, 984 | 985 | // Fetch the default set of models for this collection, resetting the 986 | // collection when they arrive. If `reset: true` is passed, the response 987 | // data will be passed through the `reset` method instead of `set`. 988 | fetch: function(options) { 989 | options = options ? _.clone(options) : {}; 990 | if (options.parse === void 0) options.parse = true; 991 | var success = options.success; 992 | var collection = this; 993 | options.success = function(resp) { 994 | var method = options.reset ? 'reset' : 'set'; 995 | collection[method](resp, options); 996 | if (success) success.call(options.context, collection, resp, options); 997 | collection.trigger('sync', collection, resp, options); 998 | }; 999 | wrapError(this, options); 1000 | return this.sync('read', this, options); 1001 | }, 1002 | 1003 | // Create a new instance of a model in this collection. Add the model to the 1004 | // collection immediately, unless `wait: true` is passed, in which case we 1005 | // wait for the server to agree. 1006 | create: function(model, options) { 1007 | options = options ? _.clone(options) : {}; 1008 | var wait = options.wait; 1009 | if (!(model = this._prepareModel(model, options))) return false; 1010 | if (!wait) this.add(model, options); 1011 | var collection = this; 1012 | var success = options.success; 1013 | options.success = function(model, resp, callbackOpts) { 1014 | if (wait) collection.add(model, callbackOpts); 1015 | if (success) success.call(callbackOpts.context, model, resp, callbackOpts); 1016 | }; 1017 | model.save(null, options); 1018 | return model; 1019 | }, 1020 | 1021 | // **parse** converts a response into a list of models to be added to the 1022 | // collection. The default implementation is just to pass it through. 1023 | parse: function(resp, options) { 1024 | return resp; 1025 | }, 1026 | 1027 | // Create a new collection with an identical list of models as this one. 1028 | clone: function() { 1029 | return new this.constructor(this.models, { 1030 | model: this.model, 1031 | comparator: this.comparator 1032 | }); 1033 | }, 1034 | 1035 | // Define how to uniquely identify models in the collection. 1036 | modelId: function (attrs) { 1037 | return attrs[this.model.prototype.idAttribute || 'id']; 1038 | }, 1039 | 1040 | // Private method to reset all internal state. Called when the collection 1041 | // is first initialized or reset. 1042 | _reset: function() { 1043 | this.length = 0; 1044 | this.models = []; 1045 | this._byId = {}; 1046 | }, 1047 | 1048 | // Prepare a hash of attributes (or other model) to be added to this 1049 | // collection. 1050 | _prepareModel: function(attrs, options) { 1051 | if (this._isModel(attrs)) { 1052 | if (!attrs.collection) attrs.collection = this; 1053 | return attrs; 1054 | } 1055 | options = options ? _.clone(options) : {}; 1056 | options.collection = this; 1057 | var model = new this.model(attrs, options); 1058 | if (!model.validationError) return model; 1059 | this.trigger('invalid', this, model.validationError, options); 1060 | return false; 1061 | }, 1062 | 1063 | // Internal method called by both remove and set. Does not trigger any 1064 | // additional events. Returns true if anything was actually removed. 1065 | _removeModels: function(models, options) { 1066 | var i, l, index, model, removed = false; 1067 | for (var i = 0, j = 0; i < models.length; i++) { 1068 | var model = models[i] = this.get(models[i]); 1069 | if (!model) continue; 1070 | var id = this.modelId(model.attributes); 1071 | if (id != null) delete this._byId[id]; 1072 | delete this._byId[model.cid]; 1073 | var index = this.indexOf(model); 1074 | this.models.splice(index, 1); 1075 | this.length--; 1076 | if (!options.silent) { 1077 | options.index = index; 1078 | model.trigger('remove', model, this, options); 1079 | } 1080 | models[j++] = model; 1081 | this._removeReference(model, options); 1082 | removed = true; 1083 | } 1084 | // We only need to slice if models array should be smaller, which is 1085 | // caused by some models not actually getting removed. 1086 | if (models.length !== j) models = models.slice(0, j); 1087 | return removed; 1088 | }, 1089 | 1090 | // Method for checking whether an object should be considered a model for 1091 | // the purposes of adding to the collection. 1092 | _isModel: function (model) { 1093 | return model instanceof Model; 1094 | }, 1095 | 1096 | // Internal method to create a model's ties to a collection. 1097 | _addReference: function(model, options) { 1098 | this._byId[model.cid] = model; 1099 | var id = this.modelId(model.attributes); 1100 | if (id != null) this._byId[id] = model; 1101 | model.on('all', this._onModelEvent, this); 1102 | }, 1103 | 1104 | // Internal method to sever a model's ties to a collection. 1105 | _removeReference: function(model, options) { 1106 | if (this === model.collection) delete model.collection; 1107 | model.off('all', this._onModelEvent, this); 1108 | }, 1109 | 1110 | // Internal method called every time a model in the set fires an event. 1111 | // Sets need to update their indexes when models change ids. All other 1112 | // events simply proxy through. "add" and "remove" events that originate 1113 | // in other collections are ignored. 1114 | _onModelEvent: function(event, model, collection, options) { 1115 | if ((event === 'add' || event === 'remove') && collection !== this) return; 1116 | if (event === 'destroy') this.remove(model, options); 1117 | if (event === 'change') { 1118 | var prevId = this.modelId(model.previousAttributes()); 1119 | var id = this.modelId(model.attributes); 1120 | if (prevId !== id) { 1121 | if (prevId != null) delete this._byId[prevId]; 1122 | if (id != null) this._byId[id] = model; 1123 | } 1124 | } 1125 | this.trigger.apply(this, arguments); 1126 | } 1127 | 1128 | }); 1129 | 1130 | // Underscore methods that we want to implement on the Collection. 1131 | // 90% of the core usefulness of Backbone Collections is actually implemented 1132 | // right here: 1133 | var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4, 1134 | foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3, 1135 | select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 2, 1136 | contains: 2, invoke: 2, max: 3, min: 3, toArray: 1, size: 1, first: 3, 1137 | head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, 1138 | without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, 1139 | isEmpty: 1, chain: 1, sample: 3, partition: 3 }; 1140 | 1141 | // Mix in each Underscore method as a proxy to `Collection#models`. 1142 | addUnderscoreMethods(Collection, collectionMethods, 'models'); 1143 | 1144 | // Underscore methods that take a property name as an argument. 1145 | var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy']; 1146 | 1147 | // Use attributes instead of properties. 1148 | _.each(attributeMethods, function(method) { 1149 | if (!_[method]) return; 1150 | Collection.prototype[method] = function(value, context) { 1151 | var iterator = _.isFunction(value) ? value : function(model) { 1152 | return model.get(value); 1153 | }; 1154 | return _[method](this.models, iterator, context); 1155 | }; 1156 | }); 1157 | 1158 | // Backbone.View 1159 | // ------------- 1160 | 1161 | // Backbone Views are almost more convention than they are actual code. A View 1162 | // is simply a JavaScript object that represents a logical chunk of UI in the 1163 | // DOM. This might be a single item, an entire list, a sidebar or panel, or 1164 | // even the surrounding frame which wraps your whole app. Defining a chunk of 1165 | // UI as a **View** allows you to define your DOM events declaratively, without 1166 | // having to worry about render order ... and makes it easy for the view to 1167 | // react to specific changes in the state of your models. 1168 | 1169 | // Creating a Backbone.View creates its initial element outside of the DOM, 1170 | // if an existing element is not provided... 1171 | var View = Backbone.View = function(options) { 1172 | this.cid = _.uniqueId('view'); 1173 | options || (options = {}); 1174 | _.extend(this, _.pick(options, viewOptions)); 1175 | this._ensureElement(); 1176 | this.initialize.apply(this, arguments); 1177 | }; 1178 | 1179 | // Cached regex to split keys for `delegate`. 1180 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 1181 | 1182 | // List of view options to be merged as properties. 1183 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 1184 | 1185 | // Set up all inheritable **Backbone.View** properties and methods. 1186 | _.extend(View.prototype, Events, { 1187 | 1188 | // The default `tagName` of a View's element is `"div"`. 1189 | tagName: 'div', 1190 | 1191 | // jQuery delegate for element lookup, scoped to DOM elements within the 1192 | // current view. This should be preferred to global lookups where possible. 1193 | $: function(selector) { 1194 | return this.$el.find(selector); 1195 | }, 1196 | 1197 | // Initialize is an empty function by default. Override it with your own 1198 | // initialization logic. 1199 | initialize: function(){}, 1200 | 1201 | // **render** is the core function that your view should override, in order 1202 | // to populate its element (`this.el`), with the appropriate HTML. The 1203 | // convention is for **render** to always return `this`. 1204 | render: function() { 1205 | return this; 1206 | }, 1207 | 1208 | // Remove this view by taking the element out of the DOM, and removing any 1209 | // applicable Backbone.Events listeners. 1210 | remove: function() { 1211 | this._removeElement(); 1212 | this.stopListening(); 1213 | return this; 1214 | }, 1215 | 1216 | // Remove this view's element from the document and all event listeners 1217 | // attached to it. Exposed for subclasses using an alternative DOM 1218 | // manipulation API. 1219 | _removeElement: function() { 1220 | this.$el.remove(); 1221 | }, 1222 | 1223 | // Change the view's element (`this.el` property) and re-delegate the 1224 | // view's events on the new element. 1225 | setElement: function(element) { 1226 | this.undelegateEvents(); 1227 | this._setElement(element); 1228 | this.delegateEvents(); 1229 | return this; 1230 | }, 1231 | 1232 | // Creates the `this.el` and `this.$el` references for this view using the 1233 | // given `el`. `el` can be a CSS selector or an HTML string, a jQuery 1234 | // context or an element. Subclasses can override this to utilize an 1235 | // alternative DOM manipulation API and are only required to set the 1236 | // `this.el` property. 1237 | _setElement: function(el) { 1238 | this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); 1239 | this.el = this.$el[0]; 1240 | }, 1241 | 1242 | // Set callbacks, where `this.events` is a hash of 1243 | // 1244 | // *{"event selector": "callback"}* 1245 | // 1246 | // { 1247 | // 'mousedown .title': 'edit', 1248 | // 'click .button': 'save', 1249 | // 'click .open': function(e) { ... } 1250 | // } 1251 | // 1252 | // pairs. Callbacks will be bound to the view, with `this` set properly. 1253 | // Uses event delegation for efficiency. 1254 | // Omitting the selector binds the event to `this.el`. 1255 | delegateEvents: function(events) { 1256 | if (!(events || (events = _.result(this, 'events')))) return this; 1257 | this.undelegateEvents(); 1258 | for (var key in events) { 1259 | var method = events[key]; 1260 | if (!_.isFunction(method)) method = this[events[key]]; 1261 | if (!method) continue; 1262 | var match = key.match(delegateEventSplitter); 1263 | this.delegate(match[1], match[2], _.bind(method, this)); 1264 | } 1265 | return this; 1266 | }, 1267 | 1268 | // Add a single event listener to the view's element (or a child element 1269 | // using `selector`). This only works for delegate-able events: not `focus`, 1270 | // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. 1271 | delegate: function(eventName, selector, listener) { 1272 | this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); 1273 | }, 1274 | 1275 | // Clears all callbacks previously bound to the view by `delegateEvents`. 1276 | // You usually don't need to use this, but may wish to if you have multiple 1277 | // Backbone views attached to the same DOM element. 1278 | undelegateEvents: function() { 1279 | if (this.$el) this.$el.off('.delegateEvents' + this.cid); 1280 | return this; 1281 | }, 1282 | 1283 | // A finer-grained `undelegateEvents` for removing a single delegated event. 1284 | // `selector` and `listener` are both optional. 1285 | undelegate: function(eventName, selector, listener) { 1286 | this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); 1287 | }, 1288 | 1289 | // Produces a DOM element to be assigned to your view. Exposed for 1290 | // subclasses using an alternative DOM manipulation API. 1291 | _createElement: function(tagName) { 1292 | return document.createElement(tagName); 1293 | }, 1294 | 1295 | // Ensure that the View has a DOM element to render into. 1296 | // If `this.el` is a string, pass it through `$()`, take the first 1297 | // matching element, and re-assign it to `el`. Otherwise, create 1298 | // an element from the `id`, `className` and `tagName` properties. 1299 | _ensureElement: function() { 1300 | if (!this.el) { 1301 | var attrs = _.extend({}, _.result(this, 'attributes')); 1302 | if (this.id) attrs.id = _.result(this, 'id'); 1303 | if (this.className) attrs['class'] = _.result(this, 'className'); 1304 | this.setElement(this._createElement(_.result(this, 'tagName'))); 1305 | this._setAttributes(attrs); 1306 | } else { 1307 | this.setElement(_.result(this, 'el')); 1308 | } 1309 | }, 1310 | 1311 | // Set attributes from a hash on this view's element. Exposed for 1312 | // subclasses using an alternative DOM manipulation API. 1313 | _setAttributes: function(attributes) { 1314 | this.$el.attr(attributes); 1315 | } 1316 | 1317 | }); 1318 | 1319 | // Backbone.sync 1320 | // ------------- 1321 | 1322 | // Override this function to change the manner in which Backbone persists 1323 | // models to the server. You will be passed the type of request, and the 1324 | // model in question. By default, makes a RESTful Ajax request 1325 | // to the model's `url()`. Some possible customizations could be: 1326 | // 1327 | // * Use `setTimeout` to batch rapid-fire updates into a single request. 1328 | // * Send up the models as XML instead of JSON. 1329 | // * Persist models via WebSockets instead of Ajax. 1330 | // 1331 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1332 | // as `POST`, with a `_method` parameter containing the true HTTP method, 1333 | // as well as all requests with the body as `application/x-www-form-urlencoded` 1334 | // instead of `application/json` with the model in a param named `model`. 1335 | // Useful when interfacing with server-side languages like **PHP** that make 1336 | // it difficult to read the body of `PUT` requests. 1337 | Backbone.sync = function(method, model, options) { 1338 | var type = methodMap[method]; 1339 | 1340 | // Default options, unless specified. 1341 | _.defaults(options || (options = {}), { 1342 | emulateHTTP: Backbone.emulateHTTP, 1343 | emulateJSON: Backbone.emulateJSON 1344 | }); 1345 | 1346 | // Default JSON-request options. 1347 | var params = {type: type, dataType: 'json'}; 1348 | 1349 | // Ensure that we have a URL. 1350 | if (!options.url) { 1351 | params.url = _.result(model, 'url') || urlError(); 1352 | } 1353 | 1354 | // Ensure that we have the appropriate request data. 1355 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1356 | params.contentType = 'application/json'; 1357 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1358 | } 1359 | 1360 | // For older servers, emulate JSON by encoding the request into an HTML-form. 1361 | if (options.emulateJSON) { 1362 | params.contentType = 'application/x-www-form-urlencoded'; 1363 | params.data = params.data ? {model: params.data} : {}; 1364 | } 1365 | 1366 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1367 | // And an `X-HTTP-Method-Override` header. 1368 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1369 | params.type = 'POST'; 1370 | if (options.emulateJSON) params.data._method = type; 1371 | var beforeSend = options.beforeSend; 1372 | options.beforeSend = function(xhr) { 1373 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 1374 | if (beforeSend) return beforeSend.apply(this, arguments); 1375 | }; 1376 | } 1377 | 1378 | // Don't process data on a non-GET request. 1379 | if (params.type !== 'GET' && !options.emulateJSON) { 1380 | params.processData = false; 1381 | } 1382 | 1383 | // Pass along `textStatus` and `errorThrown` from jQuery. 1384 | var error = options.error; 1385 | options.error = function(xhr, textStatus, errorThrown) { 1386 | options.textStatus = textStatus; 1387 | options.errorThrown = errorThrown; 1388 | if (error) error.call(options.context, xhr, textStatus, errorThrown); 1389 | }; 1390 | 1391 | // Make the request, allowing the user to override any Ajax options. 1392 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1393 | model.trigger('request', model, xhr, options); 1394 | return xhr; 1395 | }; 1396 | 1397 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1398 | var methodMap = { 1399 | 'create': 'POST', 1400 | 'update': 'PUT', 1401 | 'patch': 'PATCH', 1402 | 'delete': 'DELETE', 1403 | 'read': 'GET' 1404 | }; 1405 | 1406 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1407 | // Override this if you'd like to use a different library. 1408 | Backbone.ajax = function() { 1409 | return Backbone.$.ajax.apply(Backbone.$, arguments); 1410 | }; 1411 | 1412 | // Backbone.Router 1413 | // --------------- 1414 | 1415 | // Routers map faux-URLs to actions, and fire events when routes are 1416 | // matched. Creating a new one sets its `routes` hash, if not set statically. 1417 | var Router = Backbone.Router = function(options) { 1418 | options || (options = {}); 1419 | if (options.routes) this.routes = options.routes; 1420 | this._bindRoutes(); 1421 | this.initialize.apply(this, arguments); 1422 | }; 1423 | 1424 | // Cached regular expressions for matching named param parts and splatted 1425 | // parts of route strings. 1426 | var optionalParam = /\((.*?)\)/g; 1427 | var namedParam = /(\(\?)?:\w+/g; 1428 | var splatParam = /\*\w+/g; 1429 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1430 | 1431 | // Set up all inheritable **Backbone.Router** properties and methods. 1432 | _.extend(Router.prototype, Events, { 1433 | 1434 | // Initialize is an empty function by default. Override it with your own 1435 | // initialization logic. 1436 | initialize: function(){}, 1437 | 1438 | // Manually bind a single named route to a callback. For example: 1439 | // 1440 | // this.route('search/:query/p:num', 'search', function(query, num) { 1441 | // ... 1442 | // }); 1443 | // 1444 | route: function(route, name, callback) { 1445 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1446 | if (_.isFunction(name)) { 1447 | callback = name; 1448 | name = ''; 1449 | } 1450 | if (!callback) callback = this[name]; 1451 | var router = this; 1452 | Backbone.history.route(route, function(fragment) { 1453 | var args = router._extractParameters(route, fragment); 1454 | if (router.execute(callback, args, name) !== false) { 1455 | router.trigger.apply(router, ['route:' + name].concat(args)); 1456 | router.trigger('route', name, args); 1457 | Backbone.history.trigger('route', router, name, args); 1458 | } 1459 | }); 1460 | return this; 1461 | }, 1462 | 1463 | // Execute a route handler with the provided parameters. This is an 1464 | // excellent place to do pre-route setup or post-route cleanup. 1465 | execute: function(callback, args, name) { 1466 | if (callback) callback.apply(this, args); 1467 | }, 1468 | 1469 | // Simple proxy to `Backbone.history` to save a fragment into the history. 1470 | navigate: function(fragment, options) { 1471 | Backbone.history.navigate(fragment, options); 1472 | return this; 1473 | }, 1474 | 1475 | // Bind all defined routes to `Backbone.history`. We have to reverse the 1476 | // order of the routes here to support behavior where the most general 1477 | // routes can be defined at the bottom of the route map. 1478 | _bindRoutes: function() { 1479 | if (!this.routes) return; 1480 | this.routes = _.result(this, 'routes'); 1481 | var route, routes = _.keys(this.routes); 1482 | while ((route = routes.pop()) != null) { 1483 | this.route(route, this.routes[route]); 1484 | } 1485 | }, 1486 | 1487 | // Convert a route string into a regular expression, suitable for matching 1488 | // against the current location hash. 1489 | _routeToRegExp: function(route) { 1490 | route = route.replace(escapeRegExp, '\\$&') 1491 | .replace(optionalParam, '(?:$1)?') 1492 | .replace(namedParam, function(match, optional) { 1493 | return optional ? match : '([^/?]+)'; 1494 | }) 1495 | .replace(splatParam, '([^?]*?)'); 1496 | return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); 1497 | }, 1498 | 1499 | // Given a route, and a URL fragment that it matches, return the array of 1500 | // extracted decoded parameters. Empty or unmatched parameters will be 1501 | // treated as `null` to normalize cross-browser behavior. 1502 | _extractParameters: function(route, fragment) { 1503 | var params = route.exec(fragment).slice(1); 1504 | return _.map(params, function(param, i) { 1505 | // Don't decode the search params. 1506 | if (i === params.length - 1) return param || null; 1507 | return param ? decodeURIComponent(param) : null; 1508 | }); 1509 | } 1510 | 1511 | }); 1512 | 1513 | // Backbone.History 1514 | // ---------------- 1515 | 1516 | // Handles cross-browser history management, based on either 1517 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1518 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1519 | // and URL fragments. If the browser supports neither (old IE, natch), 1520 | // falls back to polling. 1521 | var History = Backbone.History = function() { 1522 | this.handlers = []; 1523 | _.bindAll(this, 'checkUrl'); 1524 | 1525 | // Ensure that `History` can be used outside of the browser. 1526 | if (typeof window !== 'undefined') { 1527 | this.location = window.location; 1528 | this.history = window.history; 1529 | } 1530 | }; 1531 | 1532 | // Cached regex for stripping a leading hash/slash and trailing space. 1533 | var routeStripper = /^[#\/]|\s+$/g; 1534 | 1535 | // Cached regex for stripping leading and trailing slashes. 1536 | var rootStripper = /^\/+|\/+$/g; 1537 | 1538 | // Cached regex for stripping urls of hash. 1539 | var pathStripper = /#.*$/; 1540 | 1541 | // Has the history handling already been started? 1542 | History.started = false; 1543 | 1544 | // Set up all inheritable **Backbone.History** properties and methods. 1545 | _.extend(History.prototype, Events, { 1546 | 1547 | // The default interval to poll for hash changes, if necessary, is 1548 | // twenty times a second. 1549 | interval: 50, 1550 | 1551 | // Are we at the app root? 1552 | atRoot: function() { 1553 | var path = this.location.pathname.replace(/[^\/]$/, '$&/'); 1554 | return path === this.root && !this.getSearch(); 1555 | }, 1556 | 1557 | // Does the pathname match the root? 1558 | matchRoot: function() { 1559 | var path = this.decodeFragment(this.location.pathname); 1560 | var root = path.slice(0, this.root.length - 1) + '/'; 1561 | return root === this.root; 1562 | }, 1563 | 1564 | // Unicode characters in `location.pathname` are percent encoded so they're 1565 | // decoded for comparison. `%25` should not be decoded since it may be part 1566 | // of an encoded parameter. 1567 | decodeFragment: function(fragment) { 1568 | return decodeURI(fragment.replace(/%25/g, '%2525')); 1569 | }, 1570 | 1571 | // In IE6, the hash fragment and search params are incorrect if the 1572 | // fragment contains `?`. 1573 | getSearch: function() { 1574 | var match = this.location.href.replace(/#.*/, '').match(/\?.+/); 1575 | return match ? match[0] : ''; 1576 | }, 1577 | 1578 | // Gets the true hash value. Cannot use location.hash directly due to bug 1579 | // in Firefox where location.hash will always be decoded. 1580 | getHash: function(window) { 1581 | var match = (window || this).location.href.match(/#(.*)$/); 1582 | return match ? match[1] : ''; 1583 | }, 1584 | 1585 | // Get the pathname and search params, without the root. 1586 | getPath: function() { 1587 | var path = this.decodeFragment( 1588 | this.location.pathname + this.getSearch() 1589 | ).slice(this.root.length - 1); 1590 | return path.charAt(0) === '/' ? path.slice(1) : path; 1591 | }, 1592 | 1593 | // Get the cross-browser normalized URL fragment from the path or hash. 1594 | getFragment: function(fragment) { 1595 | if (fragment == null) { 1596 | if (this._usePushState || !this._wantsHashChange) { 1597 | fragment = this.getPath(); 1598 | } else { 1599 | fragment = this.getHash(); 1600 | } 1601 | } 1602 | return fragment.replace(routeStripper, ''); 1603 | }, 1604 | 1605 | // Start the hash change handling, returning `true` if the current URL matches 1606 | // an existing route, and `false` otherwise. 1607 | start: function(options) { 1608 | if (History.started) throw new Error('Backbone.history has already been started'); 1609 | History.started = true; 1610 | 1611 | // Figure out the initial configuration. Do we need an iframe? 1612 | // Is pushState desired ... is it available? 1613 | this.options = _.extend({root: '/'}, this.options, options); 1614 | this.root = this.options.root; 1615 | this._wantsHashChange = this.options.hashChange !== false; 1616 | this._hasHashChange = 'onhashchange' in window; 1617 | this._useHashChange = this._wantsHashChange && this._hasHashChange; 1618 | this._wantsPushState = !!this.options.pushState; 1619 | this._hasPushState = !!(this.history && this.history.pushState); 1620 | this._usePushState = this._wantsPushState && this._hasPushState; 1621 | this.fragment = this.getFragment(); 1622 | 1623 | // Normalize root to always include a leading and trailing slash. 1624 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1625 | 1626 | // Transition from hashChange to pushState or vice versa if both are 1627 | // requested. 1628 | if (this._wantsHashChange && this._wantsPushState) { 1629 | 1630 | // If we've started off with a route from a `pushState`-enabled 1631 | // browser, but we're currently in a browser that doesn't support it... 1632 | if (!this._hasPushState && !this.atRoot()) { 1633 | var root = this.root.slice(0, -1) || '/'; 1634 | this.location.replace(root + '#' + this.getPath()); 1635 | // Return immediately as browser will do redirect to new url 1636 | return true; 1637 | 1638 | // Or if we've started out with a hash-based route, but we're currently 1639 | // in a browser where it could be `pushState`-based instead... 1640 | } else if (this._hasPushState && this.atRoot()) { 1641 | this.navigate(this.getHash(), {replace: true}); 1642 | } 1643 | 1644 | } 1645 | 1646 | // Proxy an iframe to handle location events if the browser doesn't 1647 | // support the `hashchange` event, HTML5 history, or the user wants 1648 | // `hashChange` but not `pushState`. 1649 | if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { 1650 | var iframe = document.createElement('iframe'); 1651 | iframe.src = 'javascript:0'; 1652 | iframe.style.display = 'none'; 1653 | iframe.tabIndex = -1; 1654 | var body = document.body; 1655 | // Using `appendChild` will throw on IE < 9 if the document is not ready. 1656 | this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow; 1657 | this.iframe.document.open().close(); 1658 | this.iframe.location.hash = '#' + this.fragment; 1659 | } 1660 | 1661 | // Add a cross-platform `addEventListener` shim for older browsers. 1662 | var addEventListener = window.addEventListener || function (eventName, listener) { 1663 | return attachEvent('on' + eventName, listener); 1664 | }; 1665 | 1666 | // Depending on whether we're using pushState or hashes, and whether 1667 | // 'onhashchange' is supported, determine how we check the URL state. 1668 | if (this._usePushState) { 1669 | addEventListener('popstate', this.checkUrl, false); 1670 | } else if (this._useHashChange && !this.iframe) { 1671 | addEventListener('hashchange', this.checkUrl, false); 1672 | } else if (this._wantsHashChange) { 1673 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval); 1674 | } 1675 | 1676 | if (!this.options.silent) return this.loadUrl(); 1677 | }, 1678 | 1679 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app, 1680 | // but possibly useful for unit testing Routers. 1681 | stop: function() { 1682 | // Add a cross-platform `removeEventListener` shim for older browsers. 1683 | var removeEventListener = window.removeEventListener || function (eventName, listener) { 1684 | return detachEvent('on' + eventName, listener); 1685 | }; 1686 | 1687 | // Remove window listeners. 1688 | if (this._usePushState) { 1689 | removeEventListener('popstate', this.checkUrl, false); 1690 | } else if (this._useHashChange && !this.iframe) { 1691 | removeEventListener('hashchange', this.checkUrl, false); 1692 | } 1693 | 1694 | // Clean up the iframe if necessary. 1695 | if (this.iframe) { 1696 | document.body.removeChild(this.iframe.frameElement); 1697 | this.iframe = null; 1698 | } 1699 | 1700 | // Some environments will throw when clearing an undefined interval. 1701 | if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); 1702 | History.started = false; 1703 | }, 1704 | 1705 | // Add a route to be tested when the fragment changes. Routes added later 1706 | // may override previous routes. 1707 | route: function(route, callback) { 1708 | this.handlers.unshift({route: route, callback: callback}); 1709 | }, 1710 | 1711 | // Checks the current URL to see if it has changed, and if it has, 1712 | // calls `loadUrl`, normalizing across the hidden iframe. 1713 | checkUrl: function(e) { 1714 | var current = this.getFragment(); 1715 | 1716 | // If the user pressed the back button, the iframe's hash will have 1717 | // changed and we should use that for comparison. 1718 | if (current === this.fragment && this.iframe) { 1719 | current = this.getHash(this.iframe); 1720 | } 1721 | 1722 | if (current === this.fragment) return false; 1723 | if (this.iframe) this.navigate(current); 1724 | this.loadUrl(); 1725 | }, 1726 | 1727 | // Attempt to load the current URL fragment. If a route succeeds with a 1728 | // match, returns `true`. If no defined routes matches the fragment, 1729 | // returns `false`. 1730 | loadUrl: function(fragment) { 1731 | // If the root doesn't match, no routes can match either. 1732 | if (!this.matchRoot()) return false; 1733 | fragment = this.fragment = this.getFragment(fragment); 1734 | return _.any(this.handlers, function(handler) { 1735 | if (handler.route.test(fragment)) { 1736 | handler.callback(fragment); 1737 | return true; 1738 | } 1739 | }); 1740 | }, 1741 | 1742 | // Save a fragment into the hash history, or replace the URL state if the 1743 | // 'replace' option is passed. You are responsible for properly URL-encoding 1744 | // the fragment in advance. 1745 | // 1746 | // The options object can contain `trigger: true` if you wish to have the 1747 | // route callback be fired (not usually desirable), or `replace: true`, if 1748 | // you wish to modify the current URL without adding an entry to the history. 1749 | navigate: function(fragment, options) { 1750 | if (!History.started) return false; 1751 | if (!options || options === true) options = {trigger: !!options}; 1752 | 1753 | // Normalize the fragment. 1754 | fragment = this.getFragment(fragment || ''); 1755 | 1756 | // Don't include a trailing slash on the root. 1757 | var root = this.root; 1758 | if (fragment === '' || fragment.charAt(0) === '?') { 1759 | root = root.slice(0, -1) || '/'; 1760 | } 1761 | var url = root + fragment; 1762 | 1763 | // Strip the hash and decode for matching. 1764 | fragment = this.decodeFragment(fragment.replace(pathStripper, '')); 1765 | 1766 | if (this.fragment === fragment) return; 1767 | this.fragment = fragment; 1768 | 1769 | // If pushState is available, we use it to set the fragment as a real URL. 1770 | if (this._usePushState) { 1771 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); 1772 | 1773 | // If hash changes haven't been explicitly disabled, update the hash 1774 | // fragment to store history. 1775 | } else if (this._wantsHashChange) { 1776 | this._updateHash(this.location, fragment, options.replace); 1777 | if (this.iframe && (fragment !== this.getHash(this.iframe))) { 1778 | // Opening and closing the iframe tricks IE7 and earlier to push a 1779 | // history entry on hash-tag change. When replace is true, we don't 1780 | // want this. 1781 | if (!options.replace) this.iframe.document.open().close(); 1782 | this._updateHash(this.iframe.location, fragment, options.replace); 1783 | } 1784 | 1785 | // If you've told us that you explicitly don't want fallback hashchange- 1786 | // based history, then `navigate` becomes a page refresh. 1787 | } else { 1788 | return this.location.assign(url); 1789 | } 1790 | if (options.trigger) return this.loadUrl(fragment); 1791 | }, 1792 | 1793 | // Update the hash location, either replacing the current entry, or adding 1794 | // a new one to the browser history. 1795 | _updateHash: function(location, fragment, replace) { 1796 | if (replace) { 1797 | var href = location.href.replace(/(javascript:|#).*$/, ''); 1798 | location.replace(href + '#' + fragment); 1799 | } else { 1800 | // Some browsers require that `hash` contains a leading #. 1801 | location.hash = '#' + fragment; 1802 | } 1803 | } 1804 | 1805 | }); 1806 | 1807 | // Create the default Backbone.history. 1808 | Backbone.history = new History; 1809 | 1810 | // Helpers 1811 | // ------- 1812 | 1813 | // Helper function to correctly set up the prototype chain for subclasses. 1814 | // Similar to `goog.inherits`, but uses a hash of prototype properties and 1815 | // class properties to be extended. 1816 | var extend = function(protoProps, staticProps) { 1817 | var parent = this; 1818 | var child; 1819 | 1820 | // The constructor function for the new subclass is either defined by you 1821 | // (the "constructor" property in your `extend` definition), or defaulted 1822 | // by us to simply call the parent constructor. 1823 | if (protoProps && _.has(protoProps, 'constructor')) { 1824 | child = protoProps.constructor; 1825 | } else { 1826 | child = function(){ return parent.apply(this, arguments); }; 1827 | } 1828 | 1829 | // Add static properties to the constructor function, if supplied. 1830 | _.extend(child, parent, staticProps); 1831 | 1832 | // Set the prototype chain to inherit from `parent`, without calling 1833 | // `parent` constructor function. 1834 | var Surrogate = function(){ this.constructor = child; }; 1835 | Surrogate.prototype = parent.prototype; 1836 | child.prototype = new Surrogate; 1837 | 1838 | // Add prototype properties (instance properties) to the subclass, 1839 | // if supplied. 1840 | if (protoProps) _.extend(child.prototype, protoProps); 1841 | 1842 | // Set a convenience property in case the parent's prototype is needed 1843 | // later. 1844 | child.__super__ = parent.prototype; 1845 | 1846 | return child; 1847 | }; 1848 | 1849 | // Set up inheritance for the model, collection, router, view and history. 1850 | Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; 1851 | 1852 | // Throw an error when a URL is needed, and none is supplied. 1853 | var urlError = function() { 1854 | throw new Error('A "url" property or function must be specified'); 1855 | }; 1856 | 1857 | // Wrap an optional error callback with a fallback error event. 1858 | var wrapError = function(model, options) { 1859 | var error = options.error; 1860 | options.error = function(resp) { 1861 | if (error) error.call(options.context, model, resp, options); 1862 | model.trigger('error', model, resp, options); 1863 | }; 1864 | }; 1865 | 1866 | return Backbone; 1867 | 1868 | })); 1869 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.8.3 2 | // http://underscorejs.org 3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `exports` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Save bytes in the minified (but not gzipped) version: 18 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 19 | 20 | // Create quick reference variables for speed access to core prototypes. 21 | var 22 | push = ArrayProto.push, 23 | slice = ArrayProto.slice, 24 | toString = ObjProto.toString, 25 | hasOwnProperty = ObjProto.hasOwnProperty; 26 | 27 | // All **ECMAScript 5** native function implementations that we hope to use 28 | // are declared here. 29 | var 30 | nativeIsArray = Array.isArray, 31 | nativeKeys = Object.keys, 32 | nativeBind = FuncProto.bind, 33 | nativeCreate = Object.create; 34 | 35 | // Naked function reference for surrogate-prototype-swapping. 36 | var Ctor = function(){}; 37 | 38 | // Create a safe reference to the Underscore object for use below. 39 | var _ = function(obj) { 40 | if (obj instanceof _) return obj; 41 | if (!(this instanceof _)) return new _(obj); 42 | this._wrapped = obj; 43 | }; 44 | 45 | // Export the Underscore object for **Node.js**, with 46 | // backwards-compatibility for the old `require()` API. If we're in 47 | // the browser, add `_` as a global object. 48 | if (typeof exports !== 'undefined') { 49 | if (typeof module !== 'undefined' && module.exports) { 50 | exports = module.exports = _; 51 | } 52 | exports._ = _; 53 | } else { 54 | root._ = _; 55 | } 56 | 57 | // Current version. 58 | _.VERSION = '1.8.3'; 59 | 60 | // Internal function that returns an efficient (for current engines) version 61 | // of the passed-in callback, to be repeatedly applied in other Underscore 62 | // functions. 63 | var optimizeCb = function(func, context, argCount) { 64 | if (context === void 0) return func; 65 | switch (argCount == null ? 3 : argCount) { 66 | case 1: return function(value) { 67 | return func.call(context, value); 68 | }; 69 | case 2: return function(value, other) { 70 | return func.call(context, value, other); 71 | }; 72 | case 3: return function(value, index, collection) { 73 | return func.call(context, value, index, collection); 74 | }; 75 | case 4: return function(accumulator, value, index, collection) { 76 | return func.call(context, accumulator, value, index, collection); 77 | }; 78 | } 79 | return function() { 80 | return func.apply(context, arguments); 81 | }; 82 | }; 83 | 84 | // A mostly-internal function to generate callbacks that can be applied 85 | // to each element in a collection, returning the desired result — either 86 | // identity, an arbitrary callback, a property matcher, or a property accessor. 87 | var cb = function(value, context, argCount) { 88 | if (value == null) return _.identity; 89 | if (_.isFunction(value)) return optimizeCb(value, context, argCount); 90 | if (_.isObject(value)) return _.matcher(value); 91 | return _.property(value); 92 | }; 93 | _.iteratee = function(value, context) { 94 | return cb(value, context, Infinity); 95 | }; 96 | 97 | // An internal function for creating assigner functions. 98 | var createAssigner = function(keysFunc, undefinedOnly) { 99 | return function(obj) { 100 | var length = arguments.length; 101 | if (length < 2 || obj == null) return obj; 102 | for (var index = 1; index < length; index++) { 103 | var source = arguments[index], 104 | keys = keysFunc(source), 105 | l = keys.length; 106 | for (var i = 0; i < l; i++) { 107 | var key = keys[i]; 108 | if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; 109 | } 110 | } 111 | return obj; 112 | }; 113 | }; 114 | 115 | // An internal function for creating a new object that inherits from another. 116 | var baseCreate = function(prototype) { 117 | if (!_.isObject(prototype)) return {}; 118 | if (nativeCreate) return nativeCreate(prototype); 119 | Ctor.prototype = prototype; 120 | var result = new Ctor; 121 | Ctor.prototype = null; 122 | return result; 123 | }; 124 | 125 | var property = function(key) { 126 | return function(obj) { 127 | return obj == null ? void 0 : obj[key]; 128 | }; 129 | }; 130 | 131 | // Helper for collection methods to determine whether a collection 132 | // should be iterated as an array or as an object 133 | // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength 134 | // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 135 | var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; 136 | var getLength = property('length'); 137 | var isArrayLike = function(collection) { 138 | var length = getLength(collection); 139 | return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; 140 | }; 141 | 142 | // Collection Functions 143 | // -------------------- 144 | 145 | // The cornerstone, an `each` implementation, aka `forEach`. 146 | // Handles raw objects in addition to array-likes. Treats all 147 | // sparse array-likes as if they were dense. 148 | _.each = _.forEach = function(obj, iteratee, context) { 149 | iteratee = optimizeCb(iteratee, context); 150 | var i, length; 151 | if (isArrayLike(obj)) { 152 | for (i = 0, length = obj.length; i < length; i++) { 153 | iteratee(obj[i], i, obj); 154 | } 155 | } else { 156 | var keys = _.keys(obj); 157 | for (i = 0, length = keys.length; i < length; i++) { 158 | iteratee(obj[keys[i]], keys[i], obj); 159 | } 160 | } 161 | return obj; 162 | }; 163 | 164 | // Return the results of applying the iteratee to each element. 165 | _.map = _.collect = function(obj, iteratee, context) { 166 | iteratee = cb(iteratee, context); 167 | var keys = !isArrayLike(obj) && _.keys(obj), 168 | length = (keys || obj).length, 169 | results = Array(length); 170 | for (var index = 0; index < length; index++) { 171 | var currentKey = keys ? keys[index] : index; 172 | results[index] = iteratee(obj[currentKey], currentKey, obj); 173 | } 174 | return results; 175 | }; 176 | 177 | // Create a reducing function iterating left or right. 178 | function createReduce(dir) { 179 | // Optimized iterator function as using arguments.length 180 | // in the main function will deoptimize the, see #1991. 181 | function iterator(obj, iteratee, memo, keys, index, length) { 182 | for (; index >= 0 && index < length; index += dir) { 183 | var currentKey = keys ? keys[index] : index; 184 | memo = iteratee(memo, obj[currentKey], currentKey, obj); 185 | } 186 | return memo; 187 | } 188 | 189 | return function(obj, iteratee, memo, context) { 190 | iteratee = optimizeCb(iteratee, context, 4); 191 | var keys = !isArrayLike(obj) && _.keys(obj), 192 | length = (keys || obj).length, 193 | index = dir > 0 ? 0 : length - 1; 194 | // Determine the initial value if none is provided. 195 | if (arguments.length < 3) { 196 | memo = obj[keys ? keys[index] : index]; 197 | index += dir; 198 | } 199 | return iterator(obj, iteratee, memo, keys, index, length); 200 | }; 201 | } 202 | 203 | // **Reduce** builds up a single result from a list of values, aka `inject`, 204 | // or `foldl`. 205 | _.reduce = _.foldl = _.inject = createReduce(1); 206 | 207 | // The right-associative version of reduce, also known as `foldr`. 208 | _.reduceRight = _.foldr = createReduce(-1); 209 | 210 | // Return the first value which passes a truth test. Aliased as `detect`. 211 | _.find = _.detect = function(obj, predicate, context) { 212 | var key; 213 | if (isArrayLike(obj)) { 214 | key = _.findIndex(obj, predicate, context); 215 | } else { 216 | key = _.findKey(obj, predicate, context); 217 | } 218 | if (key !== void 0 && key !== -1) return obj[key]; 219 | }; 220 | 221 | // Return all the elements that pass a truth test. 222 | // Aliased as `select`. 223 | _.filter = _.select = function(obj, predicate, context) { 224 | var results = []; 225 | predicate = cb(predicate, context); 226 | _.each(obj, function(value, index, list) { 227 | if (predicate(value, index, list)) results.push(value); 228 | }); 229 | return results; 230 | }; 231 | 232 | // Return all the elements for which a truth test fails. 233 | _.reject = function(obj, predicate, context) { 234 | return _.filter(obj, _.negate(cb(predicate)), context); 235 | }; 236 | 237 | // Determine whether all of the elements match a truth test. 238 | // Aliased as `all`. 239 | _.every = _.all = function(obj, predicate, context) { 240 | predicate = cb(predicate, context); 241 | var keys = !isArrayLike(obj) && _.keys(obj), 242 | length = (keys || obj).length; 243 | for (var index = 0; index < length; index++) { 244 | var currentKey = keys ? keys[index] : index; 245 | if (!predicate(obj[currentKey], currentKey, obj)) return false; 246 | } 247 | return true; 248 | }; 249 | 250 | // Determine if at least one element in the object matches a truth test. 251 | // Aliased as `any`. 252 | _.some = _.any = function(obj, predicate, context) { 253 | predicate = cb(predicate, context); 254 | var keys = !isArrayLike(obj) && _.keys(obj), 255 | length = (keys || obj).length; 256 | for (var index = 0; index < length; index++) { 257 | var currentKey = keys ? keys[index] : index; 258 | if (predicate(obj[currentKey], currentKey, obj)) return true; 259 | } 260 | return false; 261 | }; 262 | 263 | // Determine if the array or object contains a given item (using `===`). 264 | // Aliased as `includes` and `include`. 265 | _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { 266 | if (!isArrayLike(obj)) obj = _.values(obj); 267 | if (typeof fromIndex != 'number' || guard) fromIndex = 0; 268 | return _.indexOf(obj, item, fromIndex) >= 0; 269 | }; 270 | 271 | // Invoke a method (with arguments) on every item in a collection. 272 | _.invoke = function(obj, method) { 273 | var args = slice.call(arguments, 2); 274 | var isFunc = _.isFunction(method); 275 | return _.map(obj, function(value) { 276 | var func = isFunc ? method : value[method]; 277 | return func == null ? func : func.apply(value, args); 278 | }); 279 | }; 280 | 281 | // Convenience version of a common use case of `map`: fetching a property. 282 | _.pluck = function(obj, key) { 283 | return _.map(obj, _.property(key)); 284 | }; 285 | 286 | // Convenience version of a common use case of `filter`: selecting only objects 287 | // containing specific `key:value` pairs. 288 | _.where = function(obj, attrs) { 289 | return _.filter(obj, _.matcher(attrs)); 290 | }; 291 | 292 | // Convenience version of a common use case of `find`: getting the first object 293 | // containing specific `key:value` pairs. 294 | _.findWhere = function(obj, attrs) { 295 | return _.find(obj, _.matcher(attrs)); 296 | }; 297 | 298 | // Return the maximum element (or element-based computation). 299 | _.max = function(obj, iteratee, context) { 300 | var result = -Infinity, lastComputed = -Infinity, 301 | value, computed; 302 | if (iteratee == null && obj != null) { 303 | obj = isArrayLike(obj) ? obj : _.values(obj); 304 | for (var i = 0, length = obj.length; i < length; i++) { 305 | value = obj[i]; 306 | if (value > result) { 307 | result = value; 308 | } 309 | } 310 | } else { 311 | iteratee = cb(iteratee, context); 312 | _.each(obj, function(value, index, list) { 313 | computed = iteratee(value, index, list); 314 | if (computed > lastComputed || computed === -Infinity && result === -Infinity) { 315 | result = value; 316 | lastComputed = computed; 317 | } 318 | }); 319 | } 320 | return result; 321 | }; 322 | 323 | // Return the minimum element (or element-based computation). 324 | _.min = function(obj, iteratee, context) { 325 | var result = Infinity, lastComputed = Infinity, 326 | value, computed; 327 | if (iteratee == null && obj != null) { 328 | obj = isArrayLike(obj) ? obj : _.values(obj); 329 | for (var i = 0, length = obj.length; i < length; i++) { 330 | value = obj[i]; 331 | if (value < result) { 332 | result = value; 333 | } 334 | } 335 | } else { 336 | iteratee = cb(iteratee, context); 337 | _.each(obj, function(value, index, list) { 338 | computed = iteratee(value, index, list); 339 | if (computed < lastComputed || computed === Infinity && result === Infinity) { 340 | result = value; 341 | lastComputed = computed; 342 | } 343 | }); 344 | } 345 | return result; 346 | }; 347 | 348 | // Shuffle a collection, using the modern version of the 349 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 350 | _.shuffle = function(obj) { 351 | var set = isArrayLike(obj) ? obj : _.values(obj); 352 | var length = set.length; 353 | var shuffled = Array(length); 354 | for (var index = 0, rand; index < length; index++) { 355 | rand = _.random(0, index); 356 | if (rand !== index) shuffled[index] = shuffled[rand]; 357 | shuffled[rand] = set[index]; 358 | } 359 | return shuffled; 360 | }; 361 | 362 | // Sample **n** random values from a collection. 363 | // If **n** is not specified, returns a single random element. 364 | // The internal `guard` argument allows it to work with `map`. 365 | _.sample = function(obj, n, guard) { 366 | if (n == null || guard) { 367 | if (!isArrayLike(obj)) obj = _.values(obj); 368 | return obj[_.random(obj.length - 1)]; 369 | } 370 | return _.shuffle(obj).slice(0, Math.max(0, n)); 371 | }; 372 | 373 | // Sort the object's values by a criterion produced by an iteratee. 374 | _.sortBy = function(obj, iteratee, context) { 375 | iteratee = cb(iteratee, context); 376 | return _.pluck(_.map(obj, function(value, index, list) { 377 | return { 378 | value: value, 379 | index: index, 380 | criteria: iteratee(value, index, list) 381 | }; 382 | }).sort(function(left, right) { 383 | var a = left.criteria; 384 | var b = right.criteria; 385 | if (a !== b) { 386 | if (a > b || a === void 0) return 1; 387 | if (a < b || b === void 0) return -1; 388 | } 389 | return left.index - right.index; 390 | }), 'value'); 391 | }; 392 | 393 | // An internal function used for aggregate "group by" operations. 394 | var group = function(behavior) { 395 | return function(obj, iteratee, context) { 396 | var result = {}; 397 | iteratee = cb(iteratee, context); 398 | _.each(obj, function(value, index) { 399 | var key = iteratee(value, index, obj); 400 | behavior(result, value, key); 401 | }); 402 | return result; 403 | }; 404 | }; 405 | 406 | // Groups the object's values by a criterion. Pass either a string attribute 407 | // to group by, or a function that returns the criterion. 408 | _.groupBy = group(function(result, value, key) { 409 | if (_.has(result, key)) result[key].push(value); else result[key] = [value]; 410 | }); 411 | 412 | // Indexes the object's values by a criterion, similar to `groupBy`, but for 413 | // when you know that your index values will be unique. 414 | _.indexBy = group(function(result, value, key) { 415 | result[key] = value; 416 | }); 417 | 418 | // Counts instances of an object that group by a certain criterion. Pass 419 | // either a string attribute to count by, or a function that returns the 420 | // criterion. 421 | _.countBy = group(function(result, value, key) { 422 | if (_.has(result, key)) result[key]++; else result[key] = 1; 423 | }); 424 | 425 | // Safely create a real, live array from anything iterable. 426 | _.toArray = function(obj) { 427 | if (!obj) return []; 428 | if (_.isArray(obj)) return slice.call(obj); 429 | if (isArrayLike(obj)) return _.map(obj, _.identity); 430 | return _.values(obj); 431 | }; 432 | 433 | // Return the number of elements in an object. 434 | _.size = function(obj) { 435 | if (obj == null) return 0; 436 | return isArrayLike(obj) ? obj.length : _.keys(obj).length; 437 | }; 438 | 439 | // Split a collection into two arrays: one whose elements all satisfy the given 440 | // predicate, and one whose elements all do not satisfy the predicate. 441 | _.partition = function(obj, predicate, context) { 442 | predicate = cb(predicate, context); 443 | var pass = [], fail = []; 444 | _.each(obj, function(value, key, obj) { 445 | (predicate(value, key, obj) ? pass : fail).push(value); 446 | }); 447 | return [pass, fail]; 448 | }; 449 | 450 | // Array Functions 451 | // --------------- 452 | 453 | // Get the first element of an array. Passing **n** will return the first N 454 | // values in the array. Aliased as `head` and `take`. The **guard** check 455 | // allows it to work with `_.map`. 456 | _.first = _.head = _.take = function(array, n, guard) { 457 | if (array == null) return void 0; 458 | if (n == null || guard) return array[0]; 459 | return _.initial(array, array.length - n); 460 | }; 461 | 462 | // Returns everything but the last entry of the array. Especially useful on 463 | // the arguments object. Passing **n** will return all the values in 464 | // the array, excluding the last N. 465 | _.initial = function(array, n, guard) { 466 | return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); 467 | }; 468 | 469 | // Get the last element of an array. Passing **n** will return the last N 470 | // values in the array. 471 | _.last = function(array, n, guard) { 472 | if (array == null) return void 0; 473 | if (n == null || guard) return array[array.length - 1]; 474 | return _.rest(array, Math.max(0, array.length - n)); 475 | }; 476 | 477 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 478 | // Especially useful on the arguments object. Passing an **n** will return 479 | // the rest N values in the array. 480 | _.rest = _.tail = _.drop = function(array, n, guard) { 481 | return slice.call(array, n == null || guard ? 1 : n); 482 | }; 483 | 484 | // Trim out all falsy values from an array. 485 | _.compact = function(array) { 486 | return _.filter(array, _.identity); 487 | }; 488 | 489 | // Internal implementation of a recursive `flatten` function. 490 | var flatten = function(input, shallow, strict, startIndex) { 491 | var output = [], idx = 0; 492 | for (var i = startIndex || 0, length = getLength(input); i < length; i++) { 493 | var value = input[i]; 494 | if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { 495 | //flatten current level of array or arguments object 496 | if (!shallow) value = flatten(value, shallow, strict); 497 | var j = 0, len = value.length; 498 | output.length += len; 499 | while (j < len) { 500 | output[idx++] = value[j++]; 501 | } 502 | } else if (!strict) { 503 | output[idx++] = value; 504 | } 505 | } 506 | return output; 507 | }; 508 | 509 | // Flatten out an array, either recursively (by default), or just one level. 510 | _.flatten = function(array, shallow) { 511 | return flatten(array, shallow, false); 512 | }; 513 | 514 | // Return a version of the array that does not contain the specified value(s). 515 | _.without = function(array) { 516 | return _.difference(array, slice.call(arguments, 1)); 517 | }; 518 | 519 | // Produce a duplicate-free version of the array. If the array has already 520 | // been sorted, you have the option of using a faster algorithm. 521 | // Aliased as `unique`. 522 | _.uniq = _.unique = function(array, isSorted, iteratee, context) { 523 | if (!_.isBoolean(isSorted)) { 524 | context = iteratee; 525 | iteratee = isSorted; 526 | isSorted = false; 527 | } 528 | if (iteratee != null) iteratee = cb(iteratee, context); 529 | var result = []; 530 | var seen = []; 531 | for (var i = 0, length = getLength(array); i < length; i++) { 532 | var value = array[i], 533 | computed = iteratee ? iteratee(value, i, array) : value; 534 | if (isSorted) { 535 | if (!i || seen !== computed) result.push(value); 536 | seen = computed; 537 | } else if (iteratee) { 538 | if (!_.contains(seen, computed)) { 539 | seen.push(computed); 540 | result.push(value); 541 | } 542 | } else if (!_.contains(result, value)) { 543 | result.push(value); 544 | } 545 | } 546 | return result; 547 | }; 548 | 549 | // Produce an array that contains the union: each distinct element from all of 550 | // the passed-in arrays. 551 | _.union = function() { 552 | return _.uniq(flatten(arguments, true, true)); 553 | }; 554 | 555 | // Produce an array that contains every item shared between all the 556 | // passed-in arrays. 557 | _.intersection = function(array) { 558 | var result = []; 559 | var argsLength = arguments.length; 560 | for (var i = 0, length = getLength(array); i < length; i++) { 561 | var item = array[i]; 562 | if (_.contains(result, item)) continue; 563 | for (var j = 1; j < argsLength; j++) { 564 | if (!_.contains(arguments[j], item)) break; 565 | } 566 | if (j === argsLength) result.push(item); 567 | } 568 | return result; 569 | }; 570 | 571 | // Take the difference between one array and a number of other arrays. 572 | // Only the elements present in just the first array will remain. 573 | _.difference = function(array) { 574 | var rest = flatten(arguments, true, true, 1); 575 | return _.filter(array, function(value){ 576 | return !_.contains(rest, value); 577 | }); 578 | }; 579 | 580 | // Zip together multiple lists into a single array -- elements that share 581 | // an index go together. 582 | _.zip = function() { 583 | return _.unzip(arguments); 584 | }; 585 | 586 | // Complement of _.zip. Unzip accepts an array of arrays and groups 587 | // each array's elements on shared indices 588 | _.unzip = function(array) { 589 | var length = array && _.max(array, getLength).length || 0; 590 | var result = Array(length); 591 | 592 | for (var index = 0; index < length; index++) { 593 | result[index] = _.pluck(array, index); 594 | } 595 | return result; 596 | }; 597 | 598 | // Converts lists into objects. Pass either a single array of `[key, value]` 599 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 600 | // the corresponding values. 601 | _.object = function(list, values) { 602 | var result = {}; 603 | for (var i = 0, length = getLength(list); i < length; i++) { 604 | if (values) { 605 | result[list[i]] = values[i]; 606 | } else { 607 | result[list[i][0]] = list[i][1]; 608 | } 609 | } 610 | return result; 611 | }; 612 | 613 | // Generator function to create the findIndex and findLastIndex functions 614 | function createPredicateIndexFinder(dir) { 615 | return function(array, predicate, context) { 616 | predicate = cb(predicate, context); 617 | var length = getLength(array); 618 | var index = dir > 0 ? 0 : length - 1; 619 | for (; index >= 0 && index < length; index += dir) { 620 | if (predicate(array[index], index, array)) return index; 621 | } 622 | return -1; 623 | }; 624 | } 625 | 626 | // Returns the first index on an array-like that passes a predicate test 627 | _.findIndex = createPredicateIndexFinder(1); 628 | _.findLastIndex = createPredicateIndexFinder(-1); 629 | 630 | // Use a comparator function to figure out the smallest index at which 631 | // an object should be inserted so as to maintain order. Uses binary search. 632 | _.sortedIndex = function(array, obj, iteratee, context) { 633 | iteratee = cb(iteratee, context, 1); 634 | var value = iteratee(obj); 635 | var low = 0, high = getLength(array); 636 | while (low < high) { 637 | var mid = Math.floor((low + high) / 2); 638 | if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; 639 | } 640 | return low; 641 | }; 642 | 643 | // Generator function to create the indexOf and lastIndexOf functions 644 | function createIndexFinder(dir, predicateFind, sortedIndex) { 645 | return function(array, item, idx) { 646 | var i = 0, length = getLength(array); 647 | if (typeof idx == 'number') { 648 | if (dir > 0) { 649 | i = idx >= 0 ? idx : Math.max(idx + length, i); 650 | } else { 651 | length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; 652 | } 653 | } else if (sortedIndex && idx && length) { 654 | idx = sortedIndex(array, item); 655 | return array[idx] === item ? idx : -1; 656 | } 657 | if (item !== item) { 658 | idx = predicateFind(slice.call(array, i, length), _.isNaN); 659 | return idx >= 0 ? idx + i : -1; 660 | } 661 | for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { 662 | if (array[idx] === item) return idx; 663 | } 664 | return -1; 665 | }; 666 | } 667 | 668 | // Return the position of the first occurrence of an item in an array, 669 | // or -1 if the item is not included in the array. 670 | // If the array is large and already in sort order, pass `true` 671 | // for **isSorted** to use binary search. 672 | _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); 673 | _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); 674 | 675 | // Generate an integer Array containing an arithmetic progression. A port of 676 | // the native Python `range()` function. See 677 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 678 | _.range = function(start, stop, step) { 679 | if (stop == null) { 680 | stop = start || 0; 681 | start = 0; 682 | } 683 | step = step || 1; 684 | 685 | var length = Math.max(Math.ceil((stop - start) / step), 0); 686 | var range = Array(length); 687 | 688 | for (var idx = 0; idx < length; idx++, start += step) { 689 | range[idx] = start; 690 | } 691 | 692 | return range; 693 | }; 694 | 695 | // Function (ahem) Functions 696 | // ------------------ 697 | 698 | // Determines whether to execute a function as a constructor 699 | // or a normal function with the provided arguments 700 | var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { 701 | if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 702 | var self = baseCreate(sourceFunc.prototype); 703 | var result = sourceFunc.apply(self, args); 704 | if (_.isObject(result)) return result; 705 | return self; 706 | }; 707 | 708 | // Create a function bound to a given object (assigning `this`, and arguments, 709 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 710 | // available. 711 | _.bind = function(func, context) { 712 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 713 | if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); 714 | var args = slice.call(arguments, 2); 715 | var bound = function() { 716 | return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); 717 | }; 718 | return bound; 719 | }; 720 | 721 | // Partially apply a function by creating a version that has had some of its 722 | // arguments pre-filled, without changing its dynamic `this` context. _ acts 723 | // as a placeholder, allowing any combination of arguments to be pre-filled. 724 | _.partial = function(func) { 725 | var boundArgs = slice.call(arguments, 1); 726 | var bound = function() { 727 | var position = 0, length = boundArgs.length; 728 | var args = Array(length); 729 | for (var i = 0; i < length; i++) { 730 | args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; 731 | } 732 | while (position < arguments.length) args.push(arguments[position++]); 733 | return executeBound(func, bound, this, this, args); 734 | }; 735 | return bound; 736 | }; 737 | 738 | // Bind a number of an object's methods to that object. Remaining arguments 739 | // are the method names to be bound. Useful for ensuring that all callbacks 740 | // defined on an object belong to it. 741 | _.bindAll = function(obj) { 742 | var i, length = arguments.length, key; 743 | if (length <= 1) throw new Error('bindAll must be passed function names'); 744 | for (i = 1; i < length; i++) { 745 | key = arguments[i]; 746 | obj[key] = _.bind(obj[key], obj); 747 | } 748 | return obj; 749 | }; 750 | 751 | // Memoize an expensive function by storing its results. 752 | _.memoize = function(func, hasher) { 753 | var memoize = function(key) { 754 | var cache = memoize.cache; 755 | var address = '' + (hasher ? hasher.apply(this, arguments) : key); 756 | if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); 757 | return cache[address]; 758 | }; 759 | memoize.cache = {}; 760 | return memoize; 761 | }; 762 | 763 | // Delays a function for the given number of milliseconds, and then calls 764 | // it with the arguments supplied. 765 | _.delay = function(func, wait) { 766 | var args = slice.call(arguments, 2); 767 | return setTimeout(function(){ 768 | return func.apply(null, args); 769 | }, wait); 770 | }; 771 | 772 | // Defers a function, scheduling it to run after the current call stack has 773 | // cleared. 774 | _.defer = _.partial(_.delay, _, 1); 775 | 776 | // Returns a function, that, when invoked, will only be triggered at most once 777 | // during a given window of time. Normally, the throttled function will run 778 | // as much as it can, without ever going more than once per `wait` duration; 779 | // but if you'd like to disable the execution on the leading edge, pass 780 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 781 | _.throttle = function(func, wait, options) { 782 | var context, args, result; 783 | var timeout = null; 784 | var previous = 0; 785 | if (!options) options = {}; 786 | var later = function() { 787 | previous = options.leading === false ? 0 : _.now(); 788 | timeout = null; 789 | result = func.apply(context, args); 790 | if (!timeout) context = args = null; 791 | }; 792 | return function() { 793 | var now = _.now(); 794 | if (!previous && options.leading === false) previous = now; 795 | var remaining = wait - (now - previous); 796 | context = this; 797 | args = arguments; 798 | if (remaining <= 0 || remaining > wait) { 799 | if (timeout) { 800 | clearTimeout(timeout); 801 | timeout = null; 802 | } 803 | previous = now; 804 | result = func.apply(context, args); 805 | if (!timeout) context = args = null; 806 | } else if (!timeout && options.trailing !== false) { 807 | timeout = setTimeout(later, remaining); 808 | } 809 | return result; 810 | }; 811 | }; 812 | 813 | // Returns a function, that, as long as it continues to be invoked, will not 814 | // be triggered. The function will be called after it stops being called for 815 | // N milliseconds. If `immediate` is passed, trigger the function on the 816 | // leading edge, instead of the trailing. 817 | _.debounce = function(func, wait, immediate) { 818 | var timeout, args, context, timestamp, result; 819 | 820 | var later = function() { 821 | var last = _.now() - timestamp; 822 | 823 | if (last < wait && last >= 0) { 824 | timeout = setTimeout(later, wait - last); 825 | } else { 826 | timeout = null; 827 | if (!immediate) { 828 | result = func.apply(context, args); 829 | if (!timeout) context = args = null; 830 | } 831 | } 832 | }; 833 | 834 | return function() { 835 | context = this; 836 | args = arguments; 837 | timestamp = _.now(); 838 | var callNow = immediate && !timeout; 839 | if (!timeout) timeout = setTimeout(later, wait); 840 | if (callNow) { 841 | result = func.apply(context, args); 842 | context = args = null; 843 | } 844 | 845 | return result; 846 | }; 847 | }; 848 | 849 | // Returns the first function passed as an argument to the second, 850 | // allowing you to adjust arguments, run code before and after, and 851 | // conditionally execute the original function. 852 | _.wrap = function(func, wrapper) { 853 | return _.partial(wrapper, func); 854 | }; 855 | 856 | // Returns a negated version of the passed-in predicate. 857 | _.negate = function(predicate) { 858 | return function() { 859 | return !predicate.apply(this, arguments); 860 | }; 861 | }; 862 | 863 | // Returns a function that is the composition of a list of functions, each 864 | // consuming the return value of the function that follows. 865 | _.compose = function() { 866 | var args = arguments; 867 | var start = args.length - 1; 868 | return function() { 869 | var i = start; 870 | var result = args[start].apply(this, arguments); 871 | while (i--) result = args[i].call(this, result); 872 | return result; 873 | }; 874 | }; 875 | 876 | // Returns a function that will only be executed on and after the Nth call. 877 | _.after = function(times, func) { 878 | return function() { 879 | if (--times < 1) { 880 | return func.apply(this, arguments); 881 | } 882 | }; 883 | }; 884 | 885 | // Returns a function that will only be executed up to (but not including) the Nth call. 886 | _.before = function(times, func) { 887 | var memo; 888 | return function() { 889 | if (--times > 0) { 890 | memo = func.apply(this, arguments); 891 | } 892 | if (times <= 1) func = null; 893 | return memo; 894 | }; 895 | }; 896 | 897 | // Returns a function that will be executed at most one time, no matter how 898 | // often you call it. Useful for lazy initialization. 899 | _.once = _.partial(_.before, 2); 900 | 901 | // Object Functions 902 | // ---------------- 903 | 904 | // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. 905 | var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); 906 | var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', 907 | 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; 908 | 909 | function collectNonEnumProps(obj, keys) { 910 | var nonEnumIdx = nonEnumerableProps.length; 911 | var constructor = obj.constructor; 912 | var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; 913 | 914 | // Constructor is a special case. 915 | var prop = 'constructor'; 916 | if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); 917 | 918 | while (nonEnumIdx--) { 919 | prop = nonEnumerableProps[nonEnumIdx]; 920 | if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { 921 | keys.push(prop); 922 | } 923 | } 924 | } 925 | 926 | // Retrieve the names of an object's own properties. 927 | // Delegates to **ECMAScript 5**'s native `Object.keys` 928 | _.keys = function(obj) { 929 | if (!_.isObject(obj)) return []; 930 | if (nativeKeys) return nativeKeys(obj); 931 | var keys = []; 932 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 933 | // Ahem, IE < 9. 934 | if (hasEnumBug) collectNonEnumProps(obj, keys); 935 | return keys; 936 | }; 937 | 938 | // Retrieve all the property names of an object. 939 | _.allKeys = function(obj) { 940 | if (!_.isObject(obj)) return []; 941 | var keys = []; 942 | for (var key in obj) keys.push(key); 943 | // Ahem, IE < 9. 944 | if (hasEnumBug) collectNonEnumProps(obj, keys); 945 | return keys; 946 | }; 947 | 948 | // Retrieve the values of an object's properties. 949 | _.values = function(obj) { 950 | var keys = _.keys(obj); 951 | var length = keys.length; 952 | var values = Array(length); 953 | for (var i = 0; i < length; i++) { 954 | values[i] = obj[keys[i]]; 955 | } 956 | return values; 957 | }; 958 | 959 | // Returns the results of applying the iteratee to each element of the object 960 | // In contrast to _.map it returns an object 961 | _.mapObject = function(obj, iteratee, context) { 962 | iteratee = cb(iteratee, context); 963 | var keys = _.keys(obj), 964 | length = keys.length, 965 | results = {}, 966 | currentKey; 967 | for (var index = 0; index < length; index++) { 968 | currentKey = keys[index]; 969 | results[currentKey] = iteratee(obj[currentKey], currentKey, obj); 970 | } 971 | return results; 972 | }; 973 | 974 | // Convert an object into a list of `[key, value]` pairs. 975 | _.pairs = function(obj) { 976 | var keys = _.keys(obj); 977 | var length = keys.length; 978 | var pairs = Array(length); 979 | for (var i = 0; i < length; i++) { 980 | pairs[i] = [keys[i], obj[keys[i]]]; 981 | } 982 | return pairs; 983 | }; 984 | 985 | // Invert the keys and values of an object. The values must be serializable. 986 | _.invert = function(obj) { 987 | var result = {}; 988 | var keys = _.keys(obj); 989 | for (var i = 0, length = keys.length; i < length; i++) { 990 | result[obj[keys[i]]] = keys[i]; 991 | } 992 | return result; 993 | }; 994 | 995 | // Return a sorted list of the function names available on the object. 996 | // Aliased as `methods` 997 | _.functions = _.methods = function(obj) { 998 | var names = []; 999 | for (var key in obj) { 1000 | if (_.isFunction(obj[key])) names.push(key); 1001 | } 1002 | return names.sort(); 1003 | }; 1004 | 1005 | // Extend a given object with all the properties in passed-in object(s). 1006 | _.extend = createAssigner(_.allKeys); 1007 | 1008 | // Assigns a given object with all the own properties in the passed-in object(s) 1009 | // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 1010 | _.extendOwn = _.assign = createAssigner(_.keys); 1011 | 1012 | // Returns the first key on an object that passes a predicate test 1013 | _.findKey = function(obj, predicate, context) { 1014 | predicate = cb(predicate, context); 1015 | var keys = _.keys(obj), key; 1016 | for (var i = 0, length = keys.length; i < length; i++) { 1017 | key = keys[i]; 1018 | if (predicate(obj[key], key, obj)) return key; 1019 | } 1020 | }; 1021 | 1022 | // Return a copy of the object only containing the whitelisted properties. 1023 | _.pick = function(object, oiteratee, context) { 1024 | var result = {}, obj = object, iteratee, keys; 1025 | if (obj == null) return result; 1026 | if (_.isFunction(oiteratee)) { 1027 | keys = _.allKeys(obj); 1028 | iteratee = optimizeCb(oiteratee, context); 1029 | } else { 1030 | keys = flatten(arguments, false, false, 1); 1031 | iteratee = function(value, key, obj) { return key in obj; }; 1032 | obj = Object(obj); 1033 | } 1034 | for (var i = 0, length = keys.length; i < length; i++) { 1035 | var key = keys[i]; 1036 | var value = obj[key]; 1037 | if (iteratee(value, key, obj)) result[key] = value; 1038 | } 1039 | return result; 1040 | }; 1041 | 1042 | // Return a copy of the object without the blacklisted properties. 1043 | _.omit = function(obj, iteratee, context) { 1044 | if (_.isFunction(iteratee)) { 1045 | iteratee = _.negate(iteratee); 1046 | } else { 1047 | var keys = _.map(flatten(arguments, false, false, 1), String); 1048 | iteratee = function(value, key) { 1049 | return !_.contains(keys, key); 1050 | }; 1051 | } 1052 | return _.pick(obj, iteratee, context); 1053 | }; 1054 | 1055 | // Fill in a given object with default properties. 1056 | _.defaults = createAssigner(_.allKeys, true); 1057 | 1058 | // Creates an object that inherits from the given prototype object. 1059 | // If additional properties are provided then they will be added to the 1060 | // created object. 1061 | _.create = function(prototype, props) { 1062 | var result = baseCreate(prototype); 1063 | if (props) _.extendOwn(result, props); 1064 | return result; 1065 | }; 1066 | 1067 | // Create a (shallow-cloned) duplicate of an object. 1068 | _.clone = function(obj) { 1069 | if (!_.isObject(obj)) return obj; 1070 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 1071 | }; 1072 | 1073 | // Invokes interceptor with the obj, and then returns obj. 1074 | // The primary purpose of this method is to "tap into" a method chain, in 1075 | // order to perform operations on intermediate results within the chain. 1076 | _.tap = function(obj, interceptor) { 1077 | interceptor(obj); 1078 | return obj; 1079 | }; 1080 | 1081 | // Returns whether an object has a given set of `key:value` pairs. 1082 | _.isMatch = function(object, attrs) { 1083 | var keys = _.keys(attrs), length = keys.length; 1084 | if (object == null) return !length; 1085 | var obj = Object(object); 1086 | for (var i = 0; i < length; i++) { 1087 | var key = keys[i]; 1088 | if (attrs[key] !== obj[key] || !(key in obj)) return false; 1089 | } 1090 | return true; 1091 | }; 1092 | 1093 | 1094 | // Internal recursive comparison function for `isEqual`. 1095 | var eq = function(a, b, aStack, bStack) { 1096 | // Identical objects are equal. `0 === -0`, but they aren't identical. 1097 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). 1098 | if (a === b) return a !== 0 || 1 / a === 1 / b; 1099 | // A strict comparison is necessary because `null == undefined`. 1100 | if (a == null || b == null) return a === b; 1101 | // Unwrap any wrapped objects. 1102 | if (a instanceof _) a = a._wrapped; 1103 | if (b instanceof _) b = b._wrapped; 1104 | // Compare `[[Class]]` names. 1105 | var className = toString.call(a); 1106 | if (className !== toString.call(b)) return false; 1107 | switch (className) { 1108 | // Strings, numbers, regular expressions, dates, and booleans are compared by value. 1109 | case '[object RegExp]': 1110 | // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') 1111 | case '[object String]': 1112 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 1113 | // equivalent to `new String("5")`. 1114 | return '' + a === '' + b; 1115 | case '[object Number]': 1116 | // `NaN`s are equivalent, but non-reflexive. 1117 | // Object(NaN) is equivalent to NaN 1118 | if (+a !== +a) return +b !== +b; 1119 | // An `egal` comparison is performed for other numeric values. 1120 | return +a === 0 ? 1 / +a === 1 / b : +a === +b; 1121 | case '[object Date]': 1122 | case '[object Boolean]': 1123 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 1124 | // millisecond representations. Note that invalid dates with millisecond representations 1125 | // of `NaN` are not equivalent. 1126 | return +a === +b; 1127 | } 1128 | 1129 | var areArrays = className === '[object Array]'; 1130 | if (!areArrays) { 1131 | if (typeof a != 'object' || typeof b != 'object') return false; 1132 | 1133 | // Objects with different constructors are not equivalent, but `Object`s or `Array`s 1134 | // from different frames are. 1135 | var aCtor = a.constructor, bCtor = b.constructor; 1136 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && 1137 | _.isFunction(bCtor) && bCtor instanceof bCtor) 1138 | && ('constructor' in a && 'constructor' in b)) { 1139 | return false; 1140 | } 1141 | } 1142 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 1143 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 1144 | 1145 | // Initializing stack of traversed objects. 1146 | // It's done here since we only need them for objects and arrays comparison. 1147 | aStack = aStack || []; 1148 | bStack = bStack || []; 1149 | var length = aStack.length; 1150 | while (length--) { 1151 | // Linear search. Performance is inversely proportional to the number of 1152 | // unique nested structures. 1153 | if (aStack[length] === a) return bStack[length] === b; 1154 | } 1155 | 1156 | // Add the first object to the stack of traversed objects. 1157 | aStack.push(a); 1158 | bStack.push(b); 1159 | 1160 | // Recursively compare objects and arrays. 1161 | if (areArrays) { 1162 | // Compare array lengths to determine if a deep comparison is necessary. 1163 | length = a.length; 1164 | if (length !== b.length) return false; 1165 | // Deep compare the contents, ignoring non-numeric properties. 1166 | while (length--) { 1167 | if (!eq(a[length], b[length], aStack, bStack)) return false; 1168 | } 1169 | } else { 1170 | // Deep compare objects. 1171 | var keys = _.keys(a), key; 1172 | length = keys.length; 1173 | // Ensure that both objects contain the same number of properties before comparing deep equality. 1174 | if (_.keys(b).length !== length) return false; 1175 | while (length--) { 1176 | // Deep compare each member 1177 | key = keys[length]; 1178 | if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; 1179 | } 1180 | } 1181 | // Remove the first object from the stack of traversed objects. 1182 | aStack.pop(); 1183 | bStack.pop(); 1184 | return true; 1185 | }; 1186 | 1187 | // Perform a deep comparison to check if two objects are equal. 1188 | _.isEqual = function(a, b) { 1189 | return eq(a, b); 1190 | }; 1191 | 1192 | // Is a given array, string, or object empty? 1193 | // An "empty" object has no enumerable own-properties. 1194 | _.isEmpty = function(obj) { 1195 | if (obj == null) return true; 1196 | if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; 1197 | return _.keys(obj).length === 0; 1198 | }; 1199 | 1200 | // Is a given value a DOM element? 1201 | _.isElement = function(obj) { 1202 | return !!(obj && obj.nodeType === 1); 1203 | }; 1204 | 1205 | // Is a given value an array? 1206 | // Delegates to ECMA5's native Array.isArray 1207 | _.isArray = nativeIsArray || function(obj) { 1208 | return toString.call(obj) === '[object Array]'; 1209 | }; 1210 | 1211 | // Is a given variable an object? 1212 | _.isObject = function(obj) { 1213 | var type = typeof obj; 1214 | return type === 'function' || type === 'object' && !!obj; 1215 | }; 1216 | 1217 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. 1218 | _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { 1219 | _['is' + name] = function(obj) { 1220 | return toString.call(obj) === '[object ' + name + ']'; 1221 | }; 1222 | }); 1223 | 1224 | // Define a fallback version of the method in browsers (ahem, IE < 9), where 1225 | // there isn't any inspectable "Arguments" type. 1226 | if (!_.isArguments(arguments)) { 1227 | _.isArguments = function(obj) { 1228 | return _.has(obj, 'callee'); 1229 | }; 1230 | } 1231 | 1232 | // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, 1233 | // IE 11 (#1621), and in Safari 8 (#1929). 1234 | if (typeof /./ != 'function' && typeof Int8Array != 'object') { 1235 | _.isFunction = function(obj) { 1236 | return typeof obj == 'function' || false; 1237 | }; 1238 | } 1239 | 1240 | // Is a given object a finite number? 1241 | _.isFinite = function(obj) { 1242 | return isFinite(obj) && !isNaN(parseFloat(obj)); 1243 | }; 1244 | 1245 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 1246 | _.isNaN = function(obj) { 1247 | return _.isNumber(obj) && obj !== +obj; 1248 | }; 1249 | 1250 | // Is a given value a boolean? 1251 | _.isBoolean = function(obj) { 1252 | return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; 1253 | }; 1254 | 1255 | // Is a given value equal to null? 1256 | _.isNull = function(obj) { 1257 | return obj === null; 1258 | }; 1259 | 1260 | // Is a given variable undefined? 1261 | _.isUndefined = function(obj) { 1262 | return obj === void 0; 1263 | }; 1264 | 1265 | // Shortcut function for checking if an object has a given property directly 1266 | // on itself (in other words, not on a prototype). 1267 | _.has = function(obj, key) { 1268 | return obj != null && hasOwnProperty.call(obj, key); 1269 | }; 1270 | 1271 | // Utility Functions 1272 | // ----------------- 1273 | 1274 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1275 | // previous owner. Returns a reference to the Underscore object. 1276 | _.noConflict = function() { 1277 | root._ = previousUnderscore; 1278 | return this; 1279 | }; 1280 | 1281 | // Keep the identity function around for default iteratees. 1282 | _.identity = function(value) { 1283 | return value; 1284 | }; 1285 | 1286 | // Predicate-generating functions. Often useful outside of Underscore. 1287 | _.constant = function(value) { 1288 | return function() { 1289 | return value; 1290 | }; 1291 | }; 1292 | 1293 | _.noop = function(){}; 1294 | 1295 | _.property = property; 1296 | 1297 | // Generates a function for a given object that returns a given property. 1298 | _.propertyOf = function(obj) { 1299 | return obj == null ? function(){} : function(key) { 1300 | return obj[key]; 1301 | }; 1302 | }; 1303 | 1304 | // Returns a predicate for checking whether an object has a given set of 1305 | // `key:value` pairs. 1306 | _.matcher = _.matches = function(attrs) { 1307 | attrs = _.extendOwn({}, attrs); 1308 | return function(obj) { 1309 | return _.isMatch(obj, attrs); 1310 | }; 1311 | }; 1312 | 1313 | // Run a function **n** times. 1314 | _.times = function(n, iteratee, context) { 1315 | var accum = Array(Math.max(0, n)); 1316 | iteratee = optimizeCb(iteratee, context, 1); 1317 | for (var i = 0; i < n; i++) accum[i] = iteratee(i); 1318 | return accum; 1319 | }; 1320 | 1321 | // Return a random integer between min and max (inclusive). 1322 | _.random = function(min, max) { 1323 | if (max == null) { 1324 | max = min; 1325 | min = 0; 1326 | } 1327 | return min + Math.floor(Math.random() * (max - min + 1)); 1328 | }; 1329 | 1330 | // A (possibly faster) way to get the current timestamp as an integer. 1331 | _.now = Date.now || function() { 1332 | return new Date().getTime(); 1333 | }; 1334 | 1335 | // List of HTML entities for escaping. 1336 | var escapeMap = { 1337 | '&': '&', 1338 | '<': '<', 1339 | '>': '>', 1340 | '"': '"', 1341 | "'": ''', 1342 | '`': '`' 1343 | }; 1344 | var unescapeMap = _.invert(escapeMap); 1345 | 1346 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1347 | var createEscaper = function(map) { 1348 | var escaper = function(match) { 1349 | return map[match]; 1350 | }; 1351 | // Regexes for identifying a key that needs to be escaped 1352 | var source = '(?:' + _.keys(map).join('|') + ')'; 1353 | var testRegexp = RegExp(source); 1354 | var replaceRegexp = RegExp(source, 'g'); 1355 | return function(string) { 1356 | string = string == null ? '' : '' + string; 1357 | return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; 1358 | }; 1359 | }; 1360 | _.escape = createEscaper(escapeMap); 1361 | _.unescape = createEscaper(unescapeMap); 1362 | 1363 | // If the value of the named `property` is a function then invoke it with the 1364 | // `object` as context; otherwise, return it. 1365 | _.result = function(object, property, fallback) { 1366 | var value = object == null ? void 0 : object[property]; 1367 | if (value === void 0) { 1368 | value = fallback; 1369 | } 1370 | return _.isFunction(value) ? value.call(object) : value; 1371 | }; 1372 | 1373 | // Generate a unique integer id (unique within the entire client session). 1374 | // Useful for temporary DOM ids. 1375 | var idCounter = 0; 1376 | _.uniqueId = function(prefix) { 1377 | var id = ++idCounter + ''; 1378 | return prefix ? prefix + id : id; 1379 | }; 1380 | 1381 | // By default, Underscore uses ERB-style template delimiters, change the 1382 | // following template settings to use alternative delimiters. 1383 | _.templateSettings = { 1384 | evaluate : /<%([\s\S]+?)%>/g, 1385 | interpolate : /<%=([\s\S]+?)%>/g, 1386 | escape : /<%-([\s\S]+?)%>/g 1387 | }; 1388 | 1389 | // When customizing `templateSettings`, if you don't want to define an 1390 | // interpolation, evaluation or escaping regex, we need one that is 1391 | // guaranteed not to match. 1392 | var noMatch = /(.)^/; 1393 | 1394 | // Certain characters need to be escaped so that they can be put into a 1395 | // string literal. 1396 | var escapes = { 1397 | "'": "'", 1398 | '\\': '\\', 1399 | '\r': 'r', 1400 | '\n': 'n', 1401 | '\u2028': 'u2028', 1402 | '\u2029': 'u2029' 1403 | }; 1404 | 1405 | var escaper = /\\|'|\r|\n|\u2028|\u2029/g; 1406 | 1407 | var escapeChar = function(match) { 1408 | return '\\' + escapes[match]; 1409 | }; 1410 | 1411 | // JavaScript micro-templating, similar to John Resig's implementation. 1412 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1413 | // and correctly escapes quotes within interpolated code. 1414 | // NB: `oldSettings` only exists for backwards compatibility. 1415 | _.template = function(text, settings, oldSettings) { 1416 | if (!settings && oldSettings) settings = oldSettings; 1417 | settings = _.defaults({}, settings, _.templateSettings); 1418 | 1419 | // Combine delimiters into one regular expression via alternation. 1420 | var matcher = RegExp([ 1421 | (settings.escape || noMatch).source, 1422 | (settings.interpolate || noMatch).source, 1423 | (settings.evaluate || noMatch).source 1424 | ].join('|') + '|$', 'g'); 1425 | 1426 | // Compile the template source, escaping string literals appropriately. 1427 | var index = 0; 1428 | var source = "__p+='"; 1429 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1430 | source += text.slice(index, offset).replace(escaper, escapeChar); 1431 | index = offset + match.length; 1432 | 1433 | if (escape) { 1434 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1435 | } else if (interpolate) { 1436 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1437 | } else if (evaluate) { 1438 | source += "';\n" + evaluate + "\n__p+='"; 1439 | } 1440 | 1441 | // Adobe VMs need the match returned to produce the correct offest. 1442 | return match; 1443 | }); 1444 | source += "';\n"; 1445 | 1446 | // If a variable is not specified, place data values in local scope. 1447 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1448 | 1449 | source = "var __t,__p='',__j=Array.prototype.join," + 1450 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1451 | source + 'return __p;\n'; 1452 | 1453 | try { 1454 | var render = new Function(settings.variable || 'obj', '_', source); 1455 | } catch (e) { 1456 | e.source = source; 1457 | throw e; 1458 | } 1459 | 1460 | var template = function(data) { 1461 | return render.call(this, data, _); 1462 | }; 1463 | 1464 | // Provide the compiled source as a convenience for precompilation. 1465 | var argument = settings.variable || 'obj'; 1466 | template.source = 'function(' + argument + '){\n' + source + '}'; 1467 | 1468 | return template; 1469 | }; 1470 | 1471 | // Add a "chain" function. Start chaining a wrapped Underscore object. 1472 | _.chain = function(obj) { 1473 | var instance = _(obj); 1474 | instance._chain = true; 1475 | return instance; 1476 | }; 1477 | 1478 | // OOP 1479 | // --------------- 1480 | // If Underscore is called as a function, it returns a wrapped object that 1481 | // can be used OO-style. This wrapper holds altered versions of all the 1482 | // underscore functions. Wrapped objects may be chained. 1483 | 1484 | // Helper function to continue chaining intermediate results. 1485 | var result = function(instance, obj) { 1486 | return instance._chain ? _(obj).chain() : obj; 1487 | }; 1488 | 1489 | // Add your own custom functions to the Underscore object. 1490 | _.mixin = function(obj) { 1491 | _.each(_.functions(obj), function(name) { 1492 | var func = _[name] = obj[name]; 1493 | _.prototype[name] = function() { 1494 | var args = [this._wrapped]; 1495 | push.apply(args, arguments); 1496 | return result(this, func.apply(_, args)); 1497 | }; 1498 | }); 1499 | }; 1500 | 1501 | // Add all of the Underscore functions to the wrapper object. 1502 | _.mixin(_); 1503 | 1504 | // Add all mutator Array functions to the wrapper. 1505 | _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1506 | var method = ArrayProto[name]; 1507 | _.prototype[name] = function() { 1508 | var obj = this._wrapped; 1509 | method.apply(obj, arguments); 1510 | if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; 1511 | return result(this, obj); 1512 | }; 1513 | }); 1514 | 1515 | // Add all accessor Array functions to the wrapper. 1516 | _.each(['concat', 'join', 'slice'], function(name) { 1517 | var method = ArrayProto[name]; 1518 | _.prototype[name] = function() { 1519 | return result(this, method.apply(this._wrapped, arguments)); 1520 | }; 1521 | }); 1522 | 1523 | // Extracts the result from a wrapped and chained object. 1524 | _.prototype.value = function() { 1525 | return this._wrapped; 1526 | }; 1527 | 1528 | // Provide unwrapping proxy for some methods used in engine operations 1529 | // such as arithmetic and JSON stringification. 1530 | _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; 1531 | 1532 | _.prototype.toString = function() { 1533 | return '' + this._wrapped; 1534 | }; 1535 | 1536 | // AMD registration happens at the end for compatibility with AMD loaders 1537 | // that may not enforce next-turn semantics on modules. Even though general 1538 | // practice for AMD registration is to be anonymous, underscore registers 1539 | // as a named module because, like jQuery, it is a base library that is 1540 | // popular enough to be bundled in a third party lib, but not be part of 1541 | // an AMD load request. Those cases could generate an error when an 1542 | // anonymous define() is called outside of a loader request. 1543 | if (typeof define === 'function' && define.amd) { 1544 | define('underscore', [], function() { 1545 | return _; 1546 | }); 1547 | } 1548 | }.call(this)); 1549 | --------------------------------------------------------------------------------