├── log
└── .gitkeep
├── lib
├── tasks
│ └── .gitkeep
└── assets
│ ├── .gitkeep
│ └── javascripts
│ ├── underscore.js
│ └── backbone.js
├── public
├── favicon.ico
├── robots.txt
├── 500.html
├── 422.html
├── 404.html
└── index.html
├── test
├── unit
│ ├── .gitkeep
│ ├── helpers
│ │ └── books_helper_test.rb
│ └── book_test.rb
├── fixtures
│ ├── .gitkeep
│ └── books.yml
├── functional
│ ├── .gitkeep
│ └── books_controller_test.rb
├── integration
│ └── .gitkeep
├── performance
│ └── browsing_test.rb
└── test_helper.rb
├── app
├── mailers
│ └── .gitkeep
├── models
│ ├── .gitkeep
│ └── book.rb
├── assets
│ ├── javascripts
│ │ ├── models
│ │ │ ├── index.js
│ │ │ └── book.js.coffee
│ │ ├── routers
│ │ │ ├── index.js
│ │ │ └── books_router.js.coffee
│ │ ├── collections
│ │ │ ├── index.js
│ │ │ └── books.js.coffee
│ │ ├── templates
│ │ │ ├── index.js
│ │ │ └── books
│ │ │ │ ├── show.jst.ejs
│ │ │ │ ├── form.jst.ejs
│ │ │ │ └── index.jst.ejs
│ │ ├── views
│ │ │ ├── index.js
│ │ │ └── books
│ │ │ │ ├── show.js.coffee
│ │ │ │ ├── index.js.coffee
│ │ │ │ └── form.js.coffee
│ │ ├── app.js.coffee
│ │ ├── realtime.js.coffee
│ │ ├── config.js.coffee
│ │ └── application.js
│ ├── images
│ │ └── rails.png
│ └── stylesheets
│ │ ├── books.css.scss
│ │ ├── application.css
│ │ └── scaffolds.css.scss
├── helpers
│ ├── books_helper.rb
│ └── application_helper.rb
├── views
│ ├── books
│ │ ├── index.html.erb
│ │ ├── show.html.erb
│ │ ├── new.html.erb
│ │ ├── edit.html.erb
│ │ └── _form.html.erb
│ └── layouts
│ │ └── application.html.erb
└── controllers
│ ├── application_controller.rb
│ └── books_controller.rb
├── vendor
├── plugins
│ └── .gitkeep
└── assets
│ ├── javascripts
│ └── .gitkeep
│ └── stylesheets
│ └── .gitkeep
├── realtime
├── README.md
├── package.json
└── realtime-server.js
├── config
├── initializers
│ ├── redis.rb
│ ├── mime_types.rb
│ ├── backtrace_silencers.rb
│ ├── session_store.rb
│ ├── secret_token.rb
│ ├── wrap_parameters.rb
│ └── inflections.rb
├── environment.rb
├── boot.rb
├── locales
│ └── en.yml
├── environments
│ ├── development.rb
│ ├── test.rb
│ └── production.rb
├── database.yml
├── routes.rb
└── application.rb
├── config.ru
├── doc
└── README_FOR_APP
├── db
├── migrate
│ └── 20130201143533_create_books.rb
├── seeds.rb
└── schema.rb
├── Rakefile
├── script
└── rails
├── .gitignore
├── Gemfile
├── README.md
└── Gemfile.lock
/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/unit/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/mailers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/plugins/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/realtime/README.md:
--------------------------------------------------------------------------------
1 | Real Time!
--------------------------------------------------------------------------------
/test/functional/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/models/index.js:
--------------------------------------------------------------------------------
1 | //=require_tree .
--------------------------------------------------------------------------------
/app/assets/javascripts/routers/index.js:
--------------------------------------------------------------------------------
1 | //=require_tree .
--------------------------------------------------------------------------------
/app/helpers/books_helper.rb:
--------------------------------------------------------------------------------
1 | module BooksHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/assets/javascripts/collections/index.js:
--------------------------------------------------------------------------------
1 | //=require_tree .
--------------------------------------------------------------------------------
/app/assets/javascripts/templates/index.js:
--------------------------------------------------------------------------------
1 | //= require_tree .
--------------------------------------------------------------------------------
/app/assets/javascripts/views/index.js:
--------------------------------------------------------------------------------
1 | //=require_tree .
2 |
3 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/config/initializers/redis.rb:
--------------------------------------------------------------------------------
1 | $redis = Redis.new(:host => 'localhost', :port=> 6379)
--------------------------------------------------------------------------------
/app/views/books/index.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/rails.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liamks/rails-realtime/HEAD/app/assets/images/rails.png
--------------------------------------------------------------------------------
/app/views/books/show.html.erb:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/views/books/new.html.erb:
--------------------------------------------------------------------------------
1 |
New book
2 |
3 | <%= render 'form' %>
4 |
5 | <%= link_to 'Back', books_path %>
6 |
--------------------------------------------------------------------------------
/test/unit/helpers/books_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class BooksHelperTest < ActionView::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/app/views/books/edit.html.erb:
--------------------------------------------------------------------------------
1 | Editing book
2 |
3 | <%= render 'form' %>
4 |
5 | <%= link_to 'Show', @book %> |
6 | <%= link_to 'Back', books_path %>
7 |
--------------------------------------------------------------------------------
/app/assets/javascripts/models/book.js.coffee:
--------------------------------------------------------------------------------
1 | app.models.Book = Backbone.Model.extend
2 | urlRoot : '/books'
3 | defaults :
4 | title : ''
5 | num_pages : 0
--------------------------------------------------------------------------------
/test/unit/book_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class BookTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/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 RailsRealtime::Application
5 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | RailsRealtime::Application.initialize!
6 |
--------------------------------------------------------------------------------
/app/assets/javascripts/app.js.coffee:
--------------------------------------------------------------------------------
1 | $ () ->
2 | start = () ->
3 | app.realtime.connect();
4 | booksRouter = new app.routers.Books();
5 | Backbone.history.start({pushState: true});
6 |
7 | start();
--------------------------------------------------------------------------------
/app/assets/javascripts/templates/books/show.jst.ejs:
--------------------------------------------------------------------------------
1 |
2 | Title:
3 | <%= title %>
4 |
5 |
6 |
7 | Num pages:
8 | <%= num_pages %>
9 |
10 |
11 | Show All Books
--------------------------------------------------------------------------------
/app/assets/stylesheets/books.css.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the Books controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5 |
6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7 |
--------------------------------------------------------------------------------
/doc/README_FOR_APP:
--------------------------------------------------------------------------------
1 | Use this README file to introduce your application and point to useful places in the API for learning more.
2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
3 |
--------------------------------------------------------------------------------
/test/fixtures/books.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
2 |
3 | one:
4 | title: MyString
5 | num_pages: 1
6 |
7 | two:
8 | title: MyString
9 | num_pages: 1
10 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-Agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/realtime/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "RoR-real-time",
3 | "description" : "providing real-time sychronization for ruby on rails",
4 | "version" : "0.0.1",
5 | "dependencies" : {
6 | "socket.io" : "0.9.12",
7 | "redis": "0.7.3"
8 | }
9 | }
--------------------------------------------------------------------------------
/db/migrate/20130201143533_create_books.rb:
--------------------------------------------------------------------------------
1 | class CreateBooks < ActiveRecord::Migration
2 | def change
3 | create_table :books do |t|
4 | t.string :title
5 | t.integer :num_pages
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/assets/javascripts/realtime.js.coffee:
--------------------------------------------------------------------------------
1 | window.app.realtime =
2 | connect : () ->
3 | window.app.socket = io.connect('http://0.0.0.0:5001');
4 |
5 | window.app.socket.on 'rt-change', (message) ->
6 | window.app.trigger 'books', message
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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 | RailsRealtime::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/realtime/realtime-server.js:
--------------------------------------------------------------------------------
1 | var io = require('socket.io').listen(5001),
2 | redis = require('redis').createClient();
3 |
4 | redis.subscribe('rt-change');
5 |
6 | io.on('connection', function(socket){
7 | redis.on('message', function(channel, message){
8 | socket.emit('rt-change', JSON.parse(message));
9 | });
10 | });
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | RailsRealtime
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 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/app/assets/javascripts/views/books/show.js.coffee:
--------------------------------------------------------------------------------
1 | app.views.books ?= {}
2 |
3 | app.views.books.Show = Backbone.View.extend
4 | id : 'show-view'
5 |
6 | className : 'action-view'
7 |
8 | template : JST['templates/books/show']
9 |
10 | serialize : ->
11 | @model.toJSON() if @model
12 |
13 | render : ->
14 | @$el.html @template(@serialize()) if @model
15 | @$el
--------------------------------------------------------------------------------
/app/assets/javascripts/templates/books/form.jst.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Title
5 |
6 |
7 |
8 | Number of Pages
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test/performance/browsing_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'rails/performance_test_help'
3 |
4 | class BrowsingTest < ActionDispatch::PerformanceTest
5 | # Refer to the documentation for all available options
6 | # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory]
7 | # :output => 'tmp/performance', :formats => [:flat] }
8 |
9 | def test_homepage
10 | get '/'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/models/book.rb:
--------------------------------------------------------------------------------
1 | class Book < ActiveRecord::Base
2 | attr_accessible :num_pages, :title
3 | after_create {|book| book.message 'create' }
4 | after_update {|book| book.message 'update' }
5 | after_destroy {|book| book.message 'destroy' }
6 |
7 | def message action
8 | msg = { resource: 'books',
9 | action: action,
10 | id: self.id,
11 | obj: self }
12 |
13 | $redis.publish 'rt-change', msg.to_json
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | RailsRealtime::Application.config.session_store :cookie_store, key: '_rails-realtime_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 | # RailsRealtime::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/app/assets/javascripts/collections/books.js.coffee:
--------------------------------------------------------------------------------
1 | app.collections.Books = Backbone.Collection.extend
2 | model : app.models.Book
3 | url : '/books'
4 |
5 | initialize : () ->
6 | app.on 'books', @handle_change, @
7 |
8 | handle_change : (message) ->
9 | switch message.action
10 | when 'create'
11 | @add message.obj
12 | when 'update'
13 | model = @get message.id
14 | model.set message.obj
15 | when 'destroy'
16 | @remove message.obj
17 |
18 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] = "test"
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
7 | #
8 | # Note: You'll currently still have to declare fixtures explicitly in integration tests
9 | # -- they do not yet inherit this setting
10 | fixtures :all
11 |
12 | # Add more helper methods to be used by all tests here...
13 | end
14 |
--------------------------------------------------------------------------------
/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 | RailsRealtime::Application.config.secret_token = '688501f2b23da0a2411c3678cec497ed861be1cd04033c460eadc006a70b30142c5b4ce05dd80f31134e420e53cf8b017cd75e28f0be8d7e3c3ec039f30b1be9'
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile ~/.gitignore_global
6 |
7 | # Ignore bundler config
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 |
13 | # Ignore all logfiles and tempfiles.
14 | /log/*.log
15 | /tmp
16 |
17 | # ignore node modules
18 | /realtime/node_modules
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/assets/javascripts/config.js.coffee:
--------------------------------------------------------------------------------
1 | window.app =
2 | models : {}
3 | collections : {}
4 | views : {}
5 | routers : {}
6 | navigate : new Backbone.Router().navigate
7 |
8 | _.extend window.app, Backbone.Events
9 |
10 | $.ajaxSetup
11 | headers :
12 | 'X-CSRF-Token' : $('meta[name=csrf-token]').attr('content')
13 |
14 | $(document).on 'click', 'a:not([data-bypass])', (evt) ->
15 | href = $(@).attr 'href'
16 | protocol = @protocol + '//'
17 |
18 | if href.slice(protocol.length) != protocol
19 | evt.preventDefault();
20 | app.navigate href, true
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/assets/javascripts/templates/books/index.jst.ejs:
--------------------------------------------------------------------------------
1 | Listing Books
2 |
3 |
4 |
5 | Title
6 | Number of Pages
7 |
8 |
9 |
10 |
11 | <% books.each(function(book){ %>
12 |
13 | <%= book.get('title') %>
14 | <%= book.get('num_pages') %>
15 | Show
16 | Edit
17 | Destroy
18 |
19 | <% }); %>
20 |
21 | New Book
--------------------------------------------------------------------------------
/app/views/books/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for(@book) do |f| %>
2 | <% if @book.errors.any? %>
3 |
4 |
<%= pluralize(@book.errors.count, "error") %> prohibited this book from being saved:
5 |
6 |
7 | <% @book.errors.full_messages.each do |msg| %>
8 | <%= msg %>
9 | <% end %>
10 |
11 |
12 | <% end %>
13 |
14 |
15 | <%= f.label :title %>
16 | <%= f.text_field :title %>
17 |
18 |
19 | <%= f.label :num_pages %>
20 | <%= f.number_field :num_pages %>
21 |
22 |
23 | <%= f.submit %>
24 |
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/assets/javascripts/views/books/index.js.coffee:
--------------------------------------------------------------------------------
1 | app.views.books ?= {}
2 |
3 | app.views.books.Index = Backbone.View.extend
4 | id : 'index-view'
5 |
6 | className : 'action-view'
7 |
8 | template : JST['templates/books/index']
9 |
10 | events :
11 | 'click a[data-method=delete]' : 'destroy'
12 |
13 | initialize : ->
14 | @collection.on 'reset', @.render, @
15 | @collection.on 'change add remove', @.render, @
16 |
17 | destroy : (evt) ->
18 | evt.preventDefault()
19 | $a = $(evt.currentTarget)
20 | id = $a.attr('data-id')
21 | model = @collection.get id
22 | model.destroy()
23 | @collection.remove model
24 |
25 |
26 | serialize : ->
27 | books : @collection
28 |
29 | render : ->
30 | @$el.html @template(@serialize())
31 | @$el
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'rails', '3.2.11'
4 |
5 | # Bundle edge Rails instead:
6 | # gem 'rails', :git => 'git://github.com/rails/rails.git'
7 |
8 | gem 'pg'
9 | gem 'redis'
10 |
11 | # Gems used only for assets and not required
12 | # in production environments by default.
13 | group :assets do
14 | gem 'sass-rails', '~> 3.2.3'
15 | gem 'coffee-rails', '~> 3.2.1'
16 | gem 'ejs'
17 |
18 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes
19 | # gem 'therubyracer', :platforms => :ruby
20 |
21 | gem 'uglifier', '>= 1.0.3'
22 | end
23 |
24 | gem 'jquery-rails'
25 |
26 | # To use ActiveModel has_secure_password
27 | # gem 'bcrypt-ruby', '~> 3.0.0'
28 |
29 | # To use Jbuilder templates for JSON
30 | # gem 'jbuilder'
31 |
32 | gem 'thin'
33 | # Use unicorn as the app server
34 | # gem 'unicorn'
35 |
36 | # Deploy with Capistrano
37 | # gem 'capistrano'
38 |
39 | # To use debugger
40 | # gem 'debugger'
41 |
--------------------------------------------------------------------------------
/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 underscore
15 | //= require backbone
16 | //= require socket.io
17 | //
18 | //= require config
19 | //= require realtime
20 | //= require templates
21 | //
22 | //= require models
23 | //= require collections
24 | //= require views
25 | //= require routers
26 | //
27 | //= require app
28 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended to check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(:version => 20130201143533) do
15 |
16 | create_table "books", :force => true do |t|
17 | t.string "title"
18 | t.integer "num_pages"
19 | t.datetime "created_at", :null => false
20 | t.datetime "updated_at", :null => false
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/app/assets/javascripts/views/books/form.js.coffee:
--------------------------------------------------------------------------------
1 | app.views.books ?= {}
2 |
3 | app.views.books.Form = Backbone.View.extend
4 | id : 'form-view'
5 |
6 | className : 'action-view'
7 |
8 | template : JST['templates/books/form']
9 |
10 | events :
11 | 'click input[type=submit]' : 'save'
12 |
13 | initialize : ->
14 | @model = new app.models.Book()
15 |
16 | save : (evt) ->
17 | evt.preventDefault()
18 | @isNew = @model.isNew()
19 | @model.save @formValues(),
20 | success : () =>
21 | if @isNew
22 | app.collections.books.add @model
23 |
24 | @clear()
25 | app.navigate '/books/', true
26 | error :(error) =>
27 | console.log error
28 |
29 | clear : () ->
30 | @model = new app.models.Book()
31 | this.$el.find('input[type=text],input[type=number]').val('')
32 |
33 | serialize : ->
34 | @model.toJSON()
35 |
36 | formValues : ->
37 | title : this.$el.find('input[name=title]').val()
38 | num_pages : this.$el.find('input[name=num_pages]').val()
39 |
40 | render : ->
41 | @$el.html @template(@serialize())
42 | @$el
--------------------------------------------------------------------------------
/app/assets/javascripts/routers/books_router.js.coffee:
--------------------------------------------------------------------------------
1 | app.routers.Books = Backbone.Router.extend
2 | initialize : ->
3 | @books = new app.collections.Books window.books
4 | @indexView = new app.views.books.Index
5 | collection : @books
6 |
7 | @showView = new app.views.books.Show
8 | model : @books.at 0
9 |
10 | @formView = new app.views.books.Form()
11 |
12 | $('body').append @indexView.render()
13 | $('body').append @showView.render()
14 | $('body').append @formView.render()
15 |
16 | routes :
17 | "books/" : "index"
18 | "books/new" : "new"
19 | "books/:id" : "show"
20 | "books/:id/edit" : "edit"
21 |
22 | index : () ->
23 | $('.action-view').hide()
24 | @indexView.$el.show()
25 |
26 | show : (id) ->
27 | $('.action-view').hide()
28 | model = @books.get id
29 | @showView.model = model
30 | @showView.render()
31 | @showView.$el.show()
32 |
33 | edit : (id) ->
34 | $('.action-view').hide()
35 | @formView.model = @books.get id
36 | @formView.render()
37 | @formView.$el.show()
38 |
39 | new : () ->
40 | $('.action-view').hide()
41 | @formView.clear()
42 | @formView.$el.show()
--------------------------------------------------------------------------------
/test/functional/books_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class BooksControllerTest < ActionController::TestCase
4 | setup do
5 | @book = books(:one)
6 | end
7 |
8 | test "should get index" do
9 | get :index
10 | assert_response :success
11 | assert_not_nil assigns(:books)
12 | end
13 |
14 | test "should get new" do
15 | get :new
16 | assert_response :success
17 | end
18 |
19 | test "should create book" do
20 | assert_difference('Book.count') do
21 | post :create, book: { num_pages: @book.num_pages, title: @book.title }
22 | end
23 |
24 | assert_redirected_to book_path(assigns(:book))
25 | end
26 |
27 | test "should show book" do
28 | get :show, id: @book
29 | assert_response :success
30 | end
31 |
32 | test "should get edit" do
33 | get :edit, id: @book
34 | assert_response :success
35 | end
36 |
37 | test "should update book" do
38 | put :update, id: @book, book: { num_pages: @book.num_pages, title: @book.title }
39 | assert_redirected_to book_path(assigns(:book))
40 | end
41 |
42 | test "should destroy book" do
43 | assert_difference('Book.count', -1) do
44 | delete :destroy, id: @book
45 | end
46 |
47 | assert_redirected_to books_path
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/scaffolds.css.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #fff;
3 | color: #333;
4 | font-family: verdana, arial, helvetica, sans-serif;
5 | font-size: 13px;
6 | line-height: 18px;
7 | }
8 |
9 | p, ol, ul, td {
10 | font-family: verdana, arial, helvetica, sans-serif;
11 | font-size: 13px;
12 | line-height: 18px;
13 | }
14 |
15 | pre {
16 | background-color: #eee;
17 | padding: 10px;
18 | font-size: 11px;
19 | }
20 |
21 | a {
22 | color: #000;
23 | &:visited {
24 | color: #666;
25 | }
26 | &:hover {
27 | color: #fff;
28 | background-color: #000;
29 | }
30 | }
31 |
32 | div {
33 | &.field, &.actions {
34 | margin-bottom: 10px;
35 | }
36 | }
37 |
38 | #notice {
39 | color: green;
40 | }
41 |
42 | .field_with_errors {
43 | padding: 2px;
44 | background-color: red;
45 | display: table;
46 | }
47 |
48 | #error_explanation {
49 | width: 450px;
50 | border: 2px solid red;
51 | padding: 7px;
52 | padding-bottom: 0;
53 | margin-bottom: 20px;
54 | background-color: #f0f0f0;
55 | h2 {
56 | text-align: left;
57 | font-weight: bold;
58 | padding: 5px 5px 5px 15px;
59 | font-size: 12px;
60 | margin: -7px;
61 | margin-bottom: 0px;
62 | background-color: #c00;
63 | color: #fff;
64 | }
65 | ul li {
66 | font-size: 12px;
67 | list-style: square;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | RailsRealtime::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 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 8.2 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On Mac OS X with macports:
6 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
7 | # On Windows:
8 | # gem install pg
9 | # Choose the win32 build.
10 | # Install PostgreSQL and put its /bin directory on your path.
11 | #
12 | # Configure Using Gemfile
13 | # gem 'pg'
14 | #
15 | development:
16 | adapter: postgresql
17 | encoding: unicode
18 | database: rails_realtime_development
19 | pool: 5
20 | password:
21 |
22 | # Connect on a TCP socket. Omitted by default since the client uses a
23 | # domain socket that doesn't need configuration. Windows does not have
24 | # domain sockets, so uncomment these lines.
25 | #host: localhost
26 | #port: 5432
27 |
28 | # Schema search path. The server defaults to $user,public
29 | #schema_search_path: myapp,sharedapp,public
30 |
31 | # Minimum log levels, in increasing order:
32 | # debug5, debug4, debug3, debug2, debug1,
33 | # log, notice, warning, error, fatal, and panic
34 | # The server defaults to notice.
35 | #min_messages: warning
36 |
37 | # Warning: The database defined as "test" will be erased and
38 | # re-generated from your development database when you run "rake".
39 | # Do not set this db to the same as development or production.
40 | test:
41 | adapter: postgresql
42 | encoding: unicode
43 | database: rails_realtime_test
44 | pool: 5
45 | password:
46 |
47 | production:
48 | adapter: postgresql
49 | encoding: unicode
50 | database: rails_realtime_production
51 | pool: 5
52 | password:
53 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | RailsRealtime::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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Adding Real-Time To Your RESTful Rails App
2 |
3 | This repository contains the code for both the Rails app, and the Node app, that accompanies the blog entry ["Adding Real-Time To Your RESTful Rails App"](http://liamkaufman.com/blog/2013/02/27/adding-real-time-to-a-restful-rails-app/).
4 |
5 | ## Steps I Took
6 |
7 | Below are some of the steps I took, that were not outlined in the above blog entry. Make sure you have Redis installed and running!
8 |
9 | ```
10 | rails new rails_realtime --database=postgresql
11 | cd rails_realtime
12 |
13 | rake db:create
14 | rails generate scaffold Book title:string num_pages:integer
15 | rake db:migrate
16 | ```
17 |
18 | Add ```gem 'redis'``` and ```gem 'pg'``` to the Gemfile, then run ```bundle install```.
19 |
20 | ### Creating The Node.js App
21 |
22 | ```
23 | mkdir realtime
24 | cd realtime
25 | echo 'Real-Time' > README.md
26 | ```
27 |
28 | Then create ```package.json``` (see ```realtime/package.json``` for reference). From the realtime directory run ```npm install```. In your Rails' ```.gitignore``` file add ```/realtime/node_modules``` to ignore the installed node modules.
29 |
30 | ### The Backbone.js App
31 |
32 | The Backbone.js application resides in ```app/assets/javascripts```. ```application.js``` specificies the javascript files that comprise the web application and their load order. If you're building a real production app you may want to look into http://requirejs.org/ to manage your dependencies. ```app.js.coffee``` is the starting point for the Backbone.js application.
33 |
34 | ## To Start The App
35 |
36 | The Rails App: ```rails s```
37 |
38 | The Node App (from the realtime folder): ```node realtime-server.js```
39 |
40 |
41 | # Done
42 |
43 | Between the Blog, the code in this repository, and the above steps you should hopefully have the information necessary to add real-time to your Rails app!
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | RailsRealtime::Application.routes.draw do
2 | resources :books
3 |
4 |
5 | # The priority is based upon order of creation:
6 | # first created -> highest priority.
7 |
8 | # Sample of regular route:
9 | # match 'products/:id' => 'catalog#view'
10 | # Keep in mind you can assign values other than :controller and :action
11 |
12 | # Sample of named route:
13 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
14 | # This route can be invoked with purchase_url(:id => product.id)
15 |
16 | # Sample resource route (maps HTTP verbs to controller actions automatically):
17 | # resources :products
18 |
19 | # Sample resource route with options:
20 | # resources :products do
21 | # member do
22 | # get 'short'
23 | # post 'toggle'
24 | # end
25 | #
26 | # collection do
27 | # get 'sold'
28 | # end
29 | # end
30 |
31 | # Sample resource route with sub-resources:
32 | # resources :products do
33 | # resources :comments, :sales
34 | # resource :seller
35 | # end
36 |
37 | # Sample resource route with more complex sub-resources
38 | # resources :products do
39 | # resources :comments
40 | # resources :sales do
41 | # get 'recent', :on => :collection
42 | # end
43 | # end
44 |
45 | # Sample resource route within a namespace:
46 | # namespace :admin do
47 | # # Directs /admin/products/* to Admin::ProductsController
48 | # # (app/controllers/admin/products_controller.rb)
49 | # resources :products
50 | # end
51 |
52 | # You can have the root of your site routed with "root"
53 | # just remember to delete public/index.html.
54 | # root :to => 'welcome#index'
55 |
56 | # See how all your routes lay out with "rake routes"
57 |
58 | # This is a legacy wild controller route that's not recommended for RESTful applications.
59 | # Note: This route will make all actions in every controller accessible via GET requests.
60 | # match ':controller(/:action(/:id))(.:format)'
61 | end
62 |
--------------------------------------------------------------------------------
/app/controllers/books_controller.rb:
--------------------------------------------------------------------------------
1 | class BooksController < ApplicationController
2 |
3 | # GET /books
4 | # GET /books.json
5 | def index
6 | @books = Book.all
7 |
8 | respond_to do |format|
9 | format.html # index.html.erb
10 | format.json { render json: @books }
11 | end
12 | end
13 |
14 | # GET /books/1
15 | # GET /books/1.json
16 | def show
17 | @book = Book.find(params[:id])
18 |
19 | respond_to do |format|
20 | format.html { @books = Book.all }# show.html.erb
21 | format.json { render json: @book }
22 | end
23 | end
24 |
25 | # GET /books/new
26 | # GET /books/new.json
27 | def new
28 | @book = Book.new
29 |
30 | respond_to do |format|
31 | format.html {
32 | @books = Book.all
33 | render :action => 'index'
34 | }
35 | format.json { render json: @book }
36 | end
37 | end
38 |
39 | # GET /books/1/edit
40 | def edit
41 | @book = Book.find(params[:id])
42 | @books = Book.all
43 | render :action => 'index'
44 | end
45 |
46 | # POST /books
47 | # POST /books.json
48 | def create
49 | @book = Book.new(params[:book])
50 |
51 | respond_to do |format|
52 | if @book.save
53 | format.html { redirect_to @book, notice: 'Book was successfully created.' }
54 | format.json { render json: @book, status: :created, location: @book }
55 | else
56 | format.html { render action: "new" }
57 | format.json { render json: @book.errors, status: :unprocessable_entity }
58 | end
59 | end
60 | end
61 |
62 | # PUT /books/1
63 | # PUT /books/1.json
64 | def update
65 | @book = Book.find(params[:id])
66 |
67 | respond_to do |format|
68 | if @book.update_attributes(params[:book])
69 | format.html { redirect_to @book, notice: 'Book was successfully updated.' }
70 | format.json { render json: @book }
71 | else
72 | format.html { render action: "edit" }
73 | format.json { render json: @book.errors, status: :unprocessable_entity }
74 | end
75 | end
76 | end
77 |
78 | # DELETE /books/1
79 | # DELETE /books/1.json
80 | def destroy
81 | @book = Book.find(params[:id])
82 | @book.destroy
83 |
84 | respond_to do |format|
85 | format.html { redirect_to books_url }
86 | format.json { render json: @book }
87 | end
88 | end
89 |
90 |
91 |
92 | end
93 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | RailsRealtime::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 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | if defined?(Bundler)
6 | # If you precompile assets before deploying to production, use this line
7 | Bundler.require(*Rails.groups(:assets => %w(development test)))
8 | # If you want your assets lazily compiled in production, use this line
9 | # Bundler.require(:default, :assets, Rails.env)
10 | end
11 |
12 | module RailsRealtime
13 | class Application < Rails::Application
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration should go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded.
17 |
18 | # Custom directories with classes and modules you want to be autoloadable.
19 | # config.autoload_paths += %W(#{config.root}/extras)
20 |
21 | # Only load the plugins named here, in the order given (default is alphabetical).
22 | # :all can be used as a placeholder for all plugins not explicitly named.
23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24 |
25 | # Activate observers that should always be running.
26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
27 |
28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
30 | # config.time_zone = 'Central Time (US & Canada)'
31 |
32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
34 | # config.i18n.default_locale = :de
35 |
36 | # Configure the default encoding used in templates for Ruby 1.9.
37 | config.encoding = "utf-8"
38 |
39 | # Configure sensitive parameters which will be filtered from the log file.
40 | config.filter_parameters += [:password]
41 |
42 | # Enable escaping HTML in JSON.
43 | config.active_support.escape_html_entities_in_json = true
44 |
45 | # Use SQL instead of Active Record's schema dumper when creating the database.
46 | # This is necessary if your schema can't be completely dumped by the schema dumper,
47 | # like if you have constraints or database-specific column types
48 | # config.active_record.schema_format = :sql
49 |
50 | # Enforce whitelist mode for mass assignment.
51 | # This will create an empty whitelist of attributes available for mass-assignment for all models
52 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
53 | # parameters by using an attr_accessible or attr_protected declaration.
54 | config.active_record.whitelist_attributes = true
55 |
56 | # Enable the asset pipeline
57 | config.assets.enabled = true
58 |
59 | # Version of your assets, change this if you want to expire all your assets
60 | config.assets.version = '1.0'
61 |
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actionmailer (3.2.11)
5 | actionpack (= 3.2.11)
6 | mail (~> 2.4.4)
7 | actionpack (3.2.11)
8 | activemodel (= 3.2.11)
9 | activesupport (= 3.2.11)
10 | builder (~> 3.0.0)
11 | erubis (~> 2.7.0)
12 | journey (~> 1.0.4)
13 | rack (~> 1.4.0)
14 | rack-cache (~> 1.2)
15 | rack-test (~> 0.6.1)
16 | sprockets (~> 2.2.1)
17 | activemodel (3.2.11)
18 | activesupport (= 3.2.11)
19 | builder (~> 3.0.0)
20 | activerecord (3.2.11)
21 | activemodel (= 3.2.11)
22 | activesupport (= 3.2.11)
23 | arel (~> 3.0.2)
24 | tzinfo (~> 0.3.29)
25 | activeresource (3.2.11)
26 | activemodel (= 3.2.11)
27 | activesupport (= 3.2.11)
28 | activesupport (3.2.11)
29 | i18n (~> 0.6)
30 | multi_json (~> 1.0)
31 | arel (3.0.2)
32 | builder (3.0.4)
33 | coffee-rails (3.2.2)
34 | coffee-script (>= 2.2.0)
35 | railties (~> 3.2.0)
36 | coffee-script (2.2.0)
37 | coffee-script-source
38 | execjs
39 | coffee-script-source (1.4.0)
40 | daemons (1.1.9)
41 | ejs (1.1.1)
42 | erubis (2.7.0)
43 | eventmachine (1.0.0)
44 | execjs (1.4.0)
45 | multi_json (~> 1.0)
46 | hike (1.2.1)
47 | i18n (0.6.1)
48 | journey (1.0.4)
49 | jquery-rails (2.2.0)
50 | railties (>= 3.0, < 5.0)
51 | thor (>= 0.14, < 2.0)
52 | json (1.7.6)
53 | mail (2.4.4)
54 | i18n (>= 0.4.0)
55 | mime-types (~> 1.16)
56 | treetop (~> 1.4.8)
57 | mime-types (1.19)
58 | multi_json (1.5.0)
59 | pg (0.14.1)
60 | polyglot (0.3.3)
61 | rack (1.4.4)
62 | rack-cache (1.2)
63 | rack (>= 0.4)
64 | rack-ssl (1.3.3)
65 | rack
66 | rack-test (0.6.2)
67 | rack (>= 1.0)
68 | rails (3.2.11)
69 | actionmailer (= 3.2.11)
70 | actionpack (= 3.2.11)
71 | activerecord (= 3.2.11)
72 | activeresource (= 3.2.11)
73 | activesupport (= 3.2.11)
74 | bundler (~> 1.0)
75 | railties (= 3.2.11)
76 | railties (3.2.11)
77 | actionpack (= 3.2.11)
78 | activesupport (= 3.2.11)
79 | rack-ssl (~> 1.3.2)
80 | rake (>= 0.8.7)
81 | rdoc (~> 3.4)
82 | thor (>= 0.14.6, < 2.0)
83 | rake (10.0.3)
84 | rdoc (3.12)
85 | json (~> 1.4)
86 | redis (3.0.2)
87 | sass (3.2.5)
88 | sass-rails (3.2.6)
89 | railties (~> 3.2.0)
90 | sass (>= 3.1.10)
91 | tilt (~> 1.3)
92 | sprockets (2.2.2)
93 | hike (~> 1.2)
94 | multi_json (~> 1.0)
95 | rack (~> 1.0)
96 | tilt (~> 1.1, != 1.3.0)
97 | thin (1.5.0)
98 | daemons (>= 1.0.9)
99 | eventmachine (>= 0.12.6)
100 | rack (>= 1.0.0)
101 | thor (0.17.0)
102 | tilt (1.3.3)
103 | treetop (1.4.12)
104 | polyglot
105 | polyglot (>= 0.3.1)
106 | tzinfo (0.3.35)
107 | uglifier (1.3.0)
108 | execjs (>= 0.3.0)
109 | multi_json (~> 1.0, >= 1.0.2)
110 |
111 | PLATFORMS
112 | ruby
113 |
114 | DEPENDENCIES
115 | coffee-rails (~> 3.2.1)
116 | ejs
117 | jquery-rails
118 | pg
119 | rails (= 3.2.11)
120 | redis
121 | sass-rails (~> 3.2.3)
122 | thin
123 | uglifier (>= 1.0.3)
124 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ruby on Rails: Welcome aboard
5 |
174 |
187 |
188 |
189 |
190 |
203 |
204 |
205 |
209 |
210 |
214 |
215 |
216 |
Getting started
217 |
Here’s how to get rolling:
218 |
219 |
220 |
221 | Use rails generate to create your models and controllers
222 | To see all available options, run it without parameters.
223 |
224 |
225 |
226 | Set up a default route and remove public/index.html
227 | Routes are set up in config/routes.rb .
228 |
229 |
230 |
231 | Create your database
232 | Run rake db:create to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/underscore.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.4.4
2 | // http://underscorejs.org
3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
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 `global` on the server.
12 | var root = this;
13 |
14 | // Save the previous value of the `_` variable.
15 | var previousUnderscore = root._;
16 |
17 | // Establish the object that gets returned to break out of a loop iteration.
18 | var breaker = {};
19 |
20 | // Save bytes in the minified (but not gzipped) version:
21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
22 |
23 | // Create quick reference variables for speed access to core prototypes.
24 | var push = ArrayProto.push,
25 | slice = ArrayProto.slice,
26 | concat = ArrayProto.concat,
27 | toString = ObjProto.toString,
28 | hasOwnProperty = ObjProto.hasOwnProperty;
29 |
30 | // All **ECMAScript 5** native function implementations that we hope to use
31 | // are declared here.
32 | var
33 | nativeForEach = ArrayProto.forEach,
34 | nativeMap = ArrayProto.map,
35 | nativeReduce = ArrayProto.reduce,
36 | nativeReduceRight = ArrayProto.reduceRight,
37 | nativeFilter = ArrayProto.filter,
38 | nativeEvery = ArrayProto.every,
39 | nativeSome = ArrayProto.some,
40 | nativeIndexOf = ArrayProto.indexOf,
41 | nativeLastIndexOf = ArrayProto.lastIndexOf,
42 | nativeIsArray = Array.isArray,
43 | nativeKeys = Object.keys,
44 | nativeBind = FuncProto.bind;
45 |
46 | // Create a safe reference to the Underscore object for use below.
47 | var _ = function(obj) {
48 | if (obj instanceof _) return obj;
49 | if (!(this instanceof _)) return new _(obj);
50 | this._wrapped = obj;
51 | };
52 |
53 | // Export the Underscore object for **Node.js**, with
54 | // backwards-compatibility for the old `require()` API. If we're in
55 | // the browser, add `_` as a global object via a string identifier,
56 | // for Closure Compiler "advanced" mode.
57 | if (typeof exports !== 'undefined') {
58 | if (typeof module !== 'undefined' && module.exports) {
59 | exports = module.exports = _;
60 | }
61 | exports._ = _;
62 | } else {
63 | root._ = _;
64 | }
65 |
66 | // Current version.
67 | _.VERSION = '1.4.4';
68 |
69 | // Collection Functions
70 | // --------------------
71 |
72 | // The cornerstone, an `each` implementation, aka `forEach`.
73 | // Handles objects with the built-in `forEach`, arrays, and raw objects.
74 | // Delegates to **ECMAScript 5**'s native `forEach` if available.
75 | var each = _.each = _.forEach = function(obj, iterator, context) {
76 | if (obj == null) return;
77 | if (nativeForEach && obj.forEach === nativeForEach) {
78 | obj.forEach(iterator, context);
79 | } else if (obj.length === +obj.length) {
80 | for (var i = 0, l = obj.length; i < l; i++) {
81 | if (iterator.call(context, obj[i], i, obj) === breaker) return;
82 | }
83 | } else {
84 | for (var key in obj) {
85 | if (_.has(obj, key)) {
86 | if (iterator.call(context, obj[key], key, obj) === breaker) return;
87 | }
88 | }
89 | }
90 | };
91 |
92 | // Return the results of applying the iterator to each element.
93 | // Delegates to **ECMAScript 5**'s native `map` if available.
94 | _.map = _.collect = function(obj, iterator, context) {
95 | var results = [];
96 | if (obj == null) return results;
97 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
98 | each(obj, function(value, index, list) {
99 | results[results.length] = iterator.call(context, value, index, list);
100 | });
101 | return results;
102 | };
103 |
104 | var reduceError = 'Reduce of empty array with no initial value';
105 |
106 | // **Reduce** builds up a single result from a list of values, aka `inject`,
107 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
108 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
109 | var initial = arguments.length > 2;
110 | if (obj == null) obj = [];
111 | if (nativeReduce && obj.reduce === nativeReduce) {
112 | if (context) iterator = _.bind(iterator, context);
113 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
114 | }
115 | each(obj, function(value, index, list) {
116 | if (!initial) {
117 | memo = value;
118 | initial = true;
119 | } else {
120 | memo = iterator.call(context, memo, value, index, list);
121 | }
122 | });
123 | if (!initial) throw new TypeError(reduceError);
124 | return memo;
125 | };
126 |
127 | // The right-associative version of reduce, also known as `foldr`.
128 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
129 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
130 | var initial = arguments.length > 2;
131 | if (obj == null) obj = [];
132 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
133 | if (context) iterator = _.bind(iterator, context);
134 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
135 | }
136 | var length = obj.length;
137 | if (length !== +length) {
138 | var keys = _.keys(obj);
139 | length = keys.length;
140 | }
141 | each(obj, function(value, index, list) {
142 | index = keys ? keys[--length] : --length;
143 | if (!initial) {
144 | memo = obj[index];
145 | initial = true;
146 | } else {
147 | memo = iterator.call(context, memo, obj[index], index, list);
148 | }
149 | });
150 | if (!initial) throw new TypeError(reduceError);
151 | return memo;
152 | };
153 |
154 | // Return the first value which passes a truth test. Aliased as `detect`.
155 | _.find = _.detect = function(obj, iterator, context) {
156 | var result;
157 | any(obj, function(value, index, list) {
158 | if (iterator.call(context, value, index, list)) {
159 | result = value;
160 | return true;
161 | }
162 | });
163 | return result;
164 | };
165 |
166 | // Return all the elements that pass a truth test.
167 | // Delegates to **ECMAScript 5**'s native `filter` if available.
168 | // Aliased as `select`.
169 | _.filter = _.select = function(obj, iterator, context) {
170 | var results = [];
171 | if (obj == null) return results;
172 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
173 | each(obj, function(value, index, list) {
174 | if (iterator.call(context, value, index, list)) results[results.length] = value;
175 | });
176 | return results;
177 | };
178 |
179 | // Return all the elements for which a truth test fails.
180 | _.reject = function(obj, iterator, context) {
181 | return _.filter(obj, function(value, index, list) {
182 | return !iterator.call(context, value, index, list);
183 | }, context);
184 | };
185 |
186 | // Determine whether all of the elements match a truth test.
187 | // Delegates to **ECMAScript 5**'s native `every` if available.
188 | // Aliased as `all`.
189 | _.every = _.all = function(obj, iterator, context) {
190 | iterator || (iterator = _.identity);
191 | var result = true;
192 | if (obj == null) return result;
193 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
194 | each(obj, function(value, index, list) {
195 | if (!(result = result && iterator.call(context, value, index, list))) return breaker;
196 | });
197 | return !!result;
198 | };
199 |
200 | // Determine if at least one element in the object matches a truth test.
201 | // Delegates to **ECMAScript 5**'s native `some` if available.
202 | // Aliased as `any`.
203 | var any = _.some = _.any = function(obj, iterator, context) {
204 | iterator || (iterator = _.identity);
205 | var result = false;
206 | if (obj == null) return result;
207 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
208 | each(obj, function(value, index, list) {
209 | if (result || (result = iterator.call(context, value, index, list))) return breaker;
210 | });
211 | return !!result;
212 | };
213 |
214 | // Determine if the array or object contains a given value (using `===`).
215 | // Aliased as `include`.
216 | _.contains = _.include = function(obj, target) {
217 | if (obj == null) return false;
218 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
219 | return any(obj, function(value) {
220 | return value === target;
221 | });
222 | };
223 |
224 | // Invoke a method (with arguments) on every item in a collection.
225 | _.invoke = function(obj, method) {
226 | var args = slice.call(arguments, 2);
227 | var isFunc = _.isFunction(method);
228 | return _.map(obj, function(value) {
229 | return (isFunc ? method : value[method]).apply(value, args);
230 | });
231 | };
232 |
233 | // Convenience version of a common use case of `map`: fetching a property.
234 | _.pluck = function(obj, key) {
235 | return _.map(obj, function(value){ return value[key]; });
236 | };
237 |
238 | // Convenience version of a common use case of `filter`: selecting only objects
239 | // containing specific `key:value` pairs.
240 | _.where = function(obj, attrs, first) {
241 | if (_.isEmpty(attrs)) return first ? null : [];
242 | return _[first ? 'find' : 'filter'](obj, function(value) {
243 | for (var key in attrs) {
244 | if (attrs[key] !== value[key]) return false;
245 | }
246 | return true;
247 | });
248 | };
249 |
250 | // Convenience version of a common use case of `find`: getting the first object
251 | // containing specific `key:value` pairs.
252 | _.findWhere = function(obj, attrs) {
253 | return _.where(obj, attrs, true);
254 | };
255 |
256 | // Return the maximum element or (element-based computation).
257 | // Can't optimize arrays of integers longer than 65,535 elements.
258 | // See: https://bugs.webkit.org/show_bug.cgi?id=80797
259 | _.max = function(obj, iterator, context) {
260 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
261 | return Math.max.apply(Math, obj);
262 | }
263 | if (!iterator && _.isEmpty(obj)) return -Infinity;
264 | var result = {computed : -Infinity, value: -Infinity};
265 | each(obj, function(value, index, list) {
266 | var computed = iterator ? iterator.call(context, value, index, list) : value;
267 | computed >= result.computed && (result = {value : value, computed : computed});
268 | });
269 | return result.value;
270 | };
271 |
272 | // Return the minimum element (or element-based computation).
273 | _.min = function(obj, iterator, context) {
274 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
275 | return Math.min.apply(Math, obj);
276 | }
277 | if (!iterator && _.isEmpty(obj)) return Infinity;
278 | var result = {computed : Infinity, value: Infinity};
279 | each(obj, function(value, index, list) {
280 | var computed = iterator ? iterator.call(context, value, index, list) : value;
281 | computed < result.computed && (result = {value : value, computed : computed});
282 | });
283 | return result.value;
284 | };
285 |
286 | // Shuffle an array.
287 | _.shuffle = function(obj) {
288 | var rand;
289 | var index = 0;
290 | var shuffled = [];
291 | each(obj, function(value) {
292 | rand = _.random(index++);
293 | shuffled[index - 1] = shuffled[rand];
294 | shuffled[rand] = value;
295 | });
296 | return shuffled;
297 | };
298 |
299 | // An internal function to generate lookup iterators.
300 | var lookupIterator = function(value) {
301 | return _.isFunction(value) ? value : function(obj){ return obj[value]; };
302 | };
303 |
304 | // Sort the object's values by a criterion produced by an iterator.
305 | _.sortBy = function(obj, value, context) {
306 | var iterator = lookupIterator(value);
307 | return _.pluck(_.map(obj, function(value, index, list) {
308 | return {
309 | value : value,
310 | index : index,
311 | criteria : iterator.call(context, value, index, list)
312 | };
313 | }).sort(function(left, right) {
314 | var a = left.criteria;
315 | var b = right.criteria;
316 | if (a !== b) {
317 | if (a > b || a === void 0) return 1;
318 | if (a < b || b === void 0) return -1;
319 | }
320 | return left.index < right.index ? -1 : 1;
321 | }), 'value');
322 | };
323 |
324 | // An internal function used for aggregate "group by" operations.
325 | var group = function(obj, value, context, behavior) {
326 | var result = {};
327 | var iterator = lookupIterator(value || _.identity);
328 | each(obj, function(value, index) {
329 | var key = iterator.call(context, value, index, obj);
330 | behavior(result, key, value);
331 | });
332 | return result;
333 | };
334 |
335 | // Groups the object's values by a criterion. Pass either a string attribute
336 | // to group by, or a function that returns the criterion.
337 | _.groupBy = function(obj, value, context) {
338 | return group(obj, value, context, function(result, key, value) {
339 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
340 | });
341 | };
342 |
343 | // Counts instances of an object that group by a certain criterion. Pass
344 | // either a string attribute to count by, or a function that returns the
345 | // criterion.
346 | _.countBy = function(obj, value, context) {
347 | return group(obj, value, context, function(result, key) {
348 | if (!_.has(result, key)) result[key] = 0;
349 | result[key]++;
350 | });
351 | };
352 |
353 | // Use a comparator function to figure out the smallest index at which
354 | // an object should be inserted so as to maintain order. Uses binary search.
355 | _.sortedIndex = function(array, obj, iterator, context) {
356 | iterator = iterator == null ? _.identity : lookupIterator(iterator);
357 | var value = iterator.call(context, obj);
358 | var low = 0, high = array.length;
359 | while (low < high) {
360 | var mid = (low + high) >>> 1;
361 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
362 | }
363 | return low;
364 | };
365 |
366 | // Safely convert anything iterable into a real, live array.
367 | _.toArray = function(obj) {
368 | if (!obj) return [];
369 | if (_.isArray(obj)) return slice.call(obj);
370 | if (obj.length === +obj.length) return _.map(obj, _.identity);
371 | return _.values(obj);
372 | };
373 |
374 | // Return the number of elements in an object.
375 | _.size = function(obj) {
376 | if (obj == null) return 0;
377 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
378 | };
379 |
380 | // Array Functions
381 | // ---------------
382 |
383 | // Get the first element of an array. Passing **n** will return the first N
384 | // values in the array. Aliased as `head` and `take`. The **guard** check
385 | // allows it to work with `_.map`.
386 | _.first = _.head = _.take = function(array, n, guard) {
387 | if (array == null) return void 0;
388 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
389 | };
390 |
391 | // Returns everything but the last entry of the array. Especially useful on
392 | // the arguments object. Passing **n** will return all the values in
393 | // the array, excluding the last N. The **guard** check allows it to work with
394 | // `_.map`.
395 | _.initial = function(array, n, guard) {
396 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
397 | };
398 |
399 | // Get the last element of an array. Passing **n** will return the last N
400 | // values in the array. The **guard** check allows it to work with `_.map`.
401 | _.last = function(array, n, guard) {
402 | if (array == null) return void 0;
403 | if ((n != null) && !guard) {
404 | return slice.call(array, Math.max(array.length - n, 0));
405 | } else {
406 | return array[array.length - 1];
407 | }
408 | };
409 |
410 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
411 | // Especially useful on the arguments object. Passing an **n** will return
412 | // the rest N values in the array. The **guard**
413 | // check allows it to work with `_.map`.
414 | _.rest = _.tail = _.drop = function(array, n, guard) {
415 | return slice.call(array, (n == null) || guard ? 1 : n);
416 | };
417 |
418 | // Trim out all falsy values from an array.
419 | _.compact = function(array) {
420 | return _.filter(array, _.identity);
421 | };
422 |
423 | // Internal implementation of a recursive `flatten` function.
424 | var flatten = function(input, shallow, output) {
425 | each(input, function(value) {
426 | if (_.isArray(value)) {
427 | shallow ? push.apply(output, value) : flatten(value, shallow, output);
428 | } else {
429 | output.push(value);
430 | }
431 | });
432 | return output;
433 | };
434 |
435 | // Return a completely flattened version of an array.
436 | _.flatten = function(array, shallow) {
437 | return flatten(array, shallow, []);
438 | };
439 |
440 | // Return a version of the array that does not contain the specified value(s).
441 | _.without = function(array) {
442 | return _.difference(array, slice.call(arguments, 1));
443 | };
444 |
445 | // Produce a duplicate-free version of the array. If the array has already
446 | // been sorted, you have the option of using a faster algorithm.
447 | // Aliased as `unique`.
448 | _.uniq = _.unique = function(array, isSorted, iterator, context) {
449 | if (_.isFunction(isSorted)) {
450 | context = iterator;
451 | iterator = isSorted;
452 | isSorted = false;
453 | }
454 | var initial = iterator ? _.map(array, iterator, context) : array;
455 | var results = [];
456 | var seen = [];
457 | each(initial, function(value, index) {
458 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
459 | seen.push(value);
460 | results.push(array[index]);
461 | }
462 | });
463 | return results;
464 | };
465 |
466 | // Produce an array that contains the union: each distinct element from all of
467 | // the passed-in arrays.
468 | _.union = function() {
469 | return _.uniq(concat.apply(ArrayProto, arguments));
470 | };
471 |
472 | // Produce an array that contains every item shared between all the
473 | // passed-in arrays.
474 | _.intersection = function(array) {
475 | var rest = slice.call(arguments, 1);
476 | return _.filter(_.uniq(array), function(item) {
477 | return _.every(rest, function(other) {
478 | return _.indexOf(other, item) >= 0;
479 | });
480 | });
481 | };
482 |
483 | // Take the difference between one array and a number of other arrays.
484 | // Only the elements present in just the first array will remain.
485 | _.difference = function(array) {
486 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
487 | return _.filter(array, function(value){ return !_.contains(rest, value); });
488 | };
489 |
490 | // Zip together multiple lists into a single array -- elements that share
491 | // an index go together.
492 | _.zip = function() {
493 | var args = slice.call(arguments);
494 | var length = _.max(_.pluck(args, 'length'));
495 | var results = new Array(length);
496 | for (var i = 0; i < length; i++) {
497 | results[i] = _.pluck(args, "" + i);
498 | }
499 | return results;
500 | };
501 |
502 | // Converts lists into objects. Pass either a single array of `[key, value]`
503 | // pairs, or two parallel arrays of the same length -- one of keys, and one of
504 | // the corresponding values.
505 | _.object = function(list, values) {
506 | if (list == null) return {};
507 | var result = {};
508 | for (var i = 0, l = list.length; i < l; i++) {
509 | if (values) {
510 | result[list[i]] = values[i];
511 | } else {
512 | result[list[i][0]] = list[i][1];
513 | }
514 | }
515 | return result;
516 | };
517 |
518 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
519 | // we need this function. Return the position of the first occurrence of an
520 | // item in an array, or -1 if the item is not included in the array.
521 | // Delegates to **ECMAScript 5**'s native `indexOf` if available.
522 | // If the array is large and already in sort order, pass `true`
523 | // for **isSorted** to use binary search.
524 | _.indexOf = function(array, item, isSorted) {
525 | if (array == null) return -1;
526 | var i = 0, l = array.length;
527 | if (isSorted) {
528 | if (typeof isSorted == 'number') {
529 | i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
530 | } else {
531 | i = _.sortedIndex(array, item);
532 | return array[i] === item ? i : -1;
533 | }
534 | }
535 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
536 | for (; i < l; i++) if (array[i] === item) return i;
537 | return -1;
538 | };
539 |
540 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
541 | _.lastIndexOf = function(array, item, from) {
542 | if (array == null) return -1;
543 | var hasIndex = from != null;
544 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
545 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
546 | }
547 | var i = (hasIndex ? from : array.length);
548 | while (i--) if (array[i] === item) return i;
549 | return -1;
550 | };
551 |
552 | // Generate an integer Array containing an arithmetic progression. A port of
553 | // the native Python `range()` function. See
554 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
555 | _.range = function(start, stop, step) {
556 | if (arguments.length <= 1) {
557 | stop = start || 0;
558 | start = 0;
559 | }
560 | step = arguments[2] || 1;
561 |
562 | var len = Math.max(Math.ceil((stop - start) / step), 0);
563 | var idx = 0;
564 | var range = new Array(len);
565 |
566 | while(idx < len) {
567 | range[idx++] = start;
568 | start += step;
569 | }
570 |
571 | return range;
572 | };
573 |
574 | // Function (ahem) Functions
575 | // ------------------
576 |
577 | // Create a function bound to a given object (assigning `this`, and arguments,
578 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
579 | // available.
580 | _.bind = function(func, context) {
581 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
582 | var args = slice.call(arguments, 2);
583 | return function() {
584 | return func.apply(context, args.concat(slice.call(arguments)));
585 | };
586 | };
587 |
588 | // Partially apply a function by creating a version that has had some of its
589 | // arguments pre-filled, without changing its dynamic `this` context.
590 | _.partial = function(func) {
591 | var args = slice.call(arguments, 1);
592 | return function() {
593 | return func.apply(this, args.concat(slice.call(arguments)));
594 | };
595 | };
596 |
597 | // Bind all of an object's methods to that object. Useful for ensuring that
598 | // all callbacks defined on an object belong to it.
599 | _.bindAll = function(obj) {
600 | var funcs = slice.call(arguments, 1);
601 | if (funcs.length === 0) funcs = _.functions(obj);
602 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
603 | return obj;
604 | };
605 |
606 | // Memoize an expensive function by storing its results.
607 | _.memoize = function(func, hasher) {
608 | var memo = {};
609 | hasher || (hasher = _.identity);
610 | return function() {
611 | var key = hasher.apply(this, arguments);
612 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
613 | };
614 | };
615 |
616 | // Delays a function for the given number of milliseconds, and then calls
617 | // it with the arguments supplied.
618 | _.delay = function(func, wait) {
619 | var args = slice.call(arguments, 2);
620 | return setTimeout(function(){ return func.apply(null, args); }, wait);
621 | };
622 |
623 | // Defers a function, scheduling it to run after the current call stack has
624 | // cleared.
625 | _.defer = function(func) {
626 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
627 | };
628 |
629 | // Returns a function, that, when invoked, will only be triggered at most once
630 | // during a given window of time.
631 | _.throttle = function(func, wait) {
632 | var context, args, timeout, result;
633 | var previous = 0;
634 | var later = function() {
635 | previous = new Date;
636 | timeout = null;
637 | result = func.apply(context, args);
638 | };
639 | return function() {
640 | var now = new Date;
641 | var remaining = wait - (now - previous);
642 | context = this;
643 | args = arguments;
644 | if (remaining <= 0) {
645 | clearTimeout(timeout);
646 | timeout = null;
647 | previous = now;
648 | result = func.apply(context, args);
649 | } else if (!timeout) {
650 | timeout = setTimeout(later, remaining);
651 | }
652 | return result;
653 | };
654 | };
655 |
656 | // Returns a function, that, as long as it continues to be invoked, will not
657 | // be triggered. The function will be called after it stops being called for
658 | // N milliseconds. If `immediate` is passed, trigger the function on the
659 | // leading edge, instead of the trailing.
660 | _.debounce = function(func, wait, immediate) {
661 | var timeout, result;
662 | return function() {
663 | var context = this, args = arguments;
664 | var later = function() {
665 | timeout = null;
666 | if (!immediate) result = func.apply(context, args);
667 | };
668 | var callNow = immediate && !timeout;
669 | clearTimeout(timeout);
670 | timeout = setTimeout(later, wait);
671 | if (callNow) result = func.apply(context, args);
672 | return result;
673 | };
674 | };
675 |
676 | // Returns a function that will be executed at most one time, no matter how
677 | // often you call it. Useful for lazy initialization.
678 | _.once = function(func) {
679 | var ran = false, memo;
680 | return function() {
681 | if (ran) return memo;
682 | ran = true;
683 | memo = func.apply(this, arguments);
684 | func = null;
685 | return memo;
686 | };
687 | };
688 |
689 | // Returns the first function passed as an argument to the second,
690 | // allowing you to adjust arguments, run code before and after, and
691 | // conditionally execute the original function.
692 | _.wrap = function(func, wrapper) {
693 | return function() {
694 | var args = [func];
695 | push.apply(args, arguments);
696 | return wrapper.apply(this, args);
697 | };
698 | };
699 |
700 | // Returns a function that is the composition of a list of functions, each
701 | // consuming the return value of the function that follows.
702 | _.compose = function() {
703 | var funcs = arguments;
704 | return function() {
705 | var args = arguments;
706 | for (var i = funcs.length - 1; i >= 0; i--) {
707 | args = [funcs[i].apply(this, args)];
708 | }
709 | return args[0];
710 | };
711 | };
712 |
713 | // Returns a function that will only be executed after being called N times.
714 | _.after = function(times, func) {
715 | if (times <= 0) return func();
716 | return function() {
717 | if (--times < 1) {
718 | return func.apply(this, arguments);
719 | }
720 | };
721 | };
722 |
723 | // Object Functions
724 | // ----------------
725 |
726 | // Retrieve the names of an object's properties.
727 | // Delegates to **ECMAScript 5**'s native `Object.keys`
728 | _.keys = nativeKeys || function(obj) {
729 | if (obj !== Object(obj)) throw new TypeError('Invalid object');
730 | var keys = [];
731 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
732 | return keys;
733 | };
734 |
735 | // Retrieve the values of an object's properties.
736 | _.values = function(obj) {
737 | var values = [];
738 | for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
739 | return values;
740 | };
741 |
742 | // Convert an object into a list of `[key, value]` pairs.
743 | _.pairs = function(obj) {
744 | var pairs = [];
745 | for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
746 | return pairs;
747 | };
748 |
749 | // Invert the keys and values of an object. The values must be serializable.
750 | _.invert = function(obj) {
751 | var result = {};
752 | for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
753 | return result;
754 | };
755 |
756 | // Return a sorted list of the function names available on the object.
757 | // Aliased as `methods`
758 | _.functions = _.methods = function(obj) {
759 | var names = [];
760 | for (var key in obj) {
761 | if (_.isFunction(obj[key])) names.push(key);
762 | }
763 | return names.sort();
764 | };
765 |
766 | // Extend a given object with all the properties in passed-in object(s).
767 | _.extend = function(obj) {
768 | each(slice.call(arguments, 1), function(source) {
769 | if (source) {
770 | for (var prop in source) {
771 | obj[prop] = source[prop];
772 | }
773 | }
774 | });
775 | return obj;
776 | };
777 |
778 | // Return a copy of the object only containing the whitelisted properties.
779 | _.pick = function(obj) {
780 | var copy = {};
781 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
782 | each(keys, function(key) {
783 | if (key in obj) copy[key] = obj[key];
784 | });
785 | return copy;
786 | };
787 |
788 | // Return a copy of the object without the blacklisted properties.
789 | _.omit = function(obj) {
790 | var copy = {};
791 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
792 | for (var key in obj) {
793 | if (!_.contains(keys, key)) copy[key] = obj[key];
794 | }
795 | return copy;
796 | };
797 |
798 | // Fill in a given object with default properties.
799 | _.defaults = function(obj) {
800 | each(slice.call(arguments, 1), function(source) {
801 | if (source) {
802 | for (var prop in source) {
803 | if (obj[prop] == null) obj[prop] = source[prop];
804 | }
805 | }
806 | });
807 | return obj;
808 | };
809 |
810 | // Create a (shallow-cloned) duplicate of an object.
811 | _.clone = function(obj) {
812 | if (!_.isObject(obj)) return obj;
813 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
814 | };
815 |
816 | // Invokes interceptor with the obj, and then returns obj.
817 | // The primary purpose of this method is to "tap into" a method chain, in
818 | // order to perform operations on intermediate results within the chain.
819 | _.tap = function(obj, interceptor) {
820 | interceptor(obj);
821 | return obj;
822 | };
823 |
824 | // Internal recursive comparison function for `isEqual`.
825 | var eq = function(a, b, aStack, bStack) {
826 | // Identical objects are equal. `0 === -0`, but they aren't identical.
827 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
828 | if (a === b) return a !== 0 || 1 / a == 1 / b;
829 | // A strict comparison is necessary because `null == undefined`.
830 | if (a == null || b == null) return a === b;
831 | // Unwrap any wrapped objects.
832 | if (a instanceof _) a = a._wrapped;
833 | if (b instanceof _) b = b._wrapped;
834 | // Compare `[[Class]]` names.
835 | var className = toString.call(a);
836 | if (className != toString.call(b)) return false;
837 | switch (className) {
838 | // Strings, numbers, dates, and booleans are compared by value.
839 | case '[object String]':
840 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
841 | // equivalent to `new String("5")`.
842 | return a == String(b);
843 | case '[object Number]':
844 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
845 | // other numeric values.
846 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
847 | case '[object Date]':
848 | case '[object Boolean]':
849 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their
850 | // millisecond representations. Note that invalid dates with millisecond representations
851 | // of `NaN` are not equivalent.
852 | return +a == +b;
853 | // RegExps are compared by their source patterns and flags.
854 | case '[object RegExp]':
855 | return a.source == b.source &&
856 | a.global == b.global &&
857 | a.multiline == b.multiline &&
858 | a.ignoreCase == b.ignoreCase;
859 | }
860 | if (typeof a != 'object' || typeof b != 'object') return false;
861 | // Assume equality for cyclic structures. The algorithm for detecting cyclic
862 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
863 | var length = aStack.length;
864 | while (length--) {
865 | // Linear search. Performance is inversely proportional to the number of
866 | // unique nested structures.
867 | if (aStack[length] == a) return bStack[length] == b;
868 | }
869 | // Add the first object to the stack of traversed objects.
870 | aStack.push(a);
871 | bStack.push(b);
872 | var size = 0, result = true;
873 | // Recursively compare objects and arrays.
874 | if (className == '[object Array]') {
875 | // Compare array lengths to determine if a deep comparison is necessary.
876 | size = a.length;
877 | result = size == b.length;
878 | if (result) {
879 | // Deep compare the contents, ignoring non-numeric properties.
880 | while (size--) {
881 | if (!(result = eq(a[size], b[size], aStack, bStack))) break;
882 | }
883 | }
884 | } else {
885 | // Objects with different constructors are not equivalent, but `Object`s
886 | // from different frames are.
887 | var aCtor = a.constructor, bCtor = b.constructor;
888 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
889 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
890 | return false;
891 | }
892 | // Deep compare objects.
893 | for (var key in a) {
894 | if (_.has(a, key)) {
895 | // Count the expected number of properties.
896 | size++;
897 | // Deep compare each member.
898 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
899 | }
900 | }
901 | // Ensure that both objects contain the same number of properties.
902 | if (result) {
903 | for (key in b) {
904 | if (_.has(b, key) && !(size--)) break;
905 | }
906 | result = !size;
907 | }
908 | }
909 | // Remove the first object from the stack of traversed objects.
910 | aStack.pop();
911 | bStack.pop();
912 | return result;
913 | };
914 |
915 | // Perform a deep comparison to check if two objects are equal.
916 | _.isEqual = function(a, b) {
917 | return eq(a, b, [], []);
918 | };
919 |
920 | // Is a given array, string, or object empty?
921 | // An "empty" object has no enumerable own-properties.
922 | _.isEmpty = function(obj) {
923 | if (obj == null) return true;
924 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
925 | for (var key in obj) if (_.has(obj, key)) return false;
926 | return true;
927 | };
928 |
929 | // Is a given value a DOM element?
930 | _.isElement = function(obj) {
931 | return !!(obj && obj.nodeType === 1);
932 | };
933 |
934 | // Is a given value an array?
935 | // Delegates to ECMA5's native Array.isArray
936 | _.isArray = nativeIsArray || function(obj) {
937 | return toString.call(obj) == '[object Array]';
938 | };
939 |
940 | // Is a given variable an object?
941 | _.isObject = function(obj) {
942 | return obj === Object(obj);
943 | };
944 |
945 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
946 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
947 | _['is' + name] = function(obj) {
948 | return toString.call(obj) == '[object ' + name + ']';
949 | };
950 | });
951 |
952 | // Define a fallback version of the method in browsers (ahem, IE), where
953 | // there isn't any inspectable "Arguments" type.
954 | if (!_.isArguments(arguments)) {
955 | _.isArguments = function(obj) {
956 | return !!(obj && _.has(obj, 'callee'));
957 | };
958 | }
959 |
960 | // Optimize `isFunction` if appropriate.
961 | if (typeof (/./) !== 'function') {
962 | _.isFunction = function(obj) {
963 | return typeof obj === 'function';
964 | };
965 | }
966 |
967 | // Is a given object a finite number?
968 | _.isFinite = function(obj) {
969 | return isFinite(obj) && !isNaN(parseFloat(obj));
970 | };
971 |
972 | // Is the given value `NaN`? (NaN is the only number which does not equal itself).
973 | _.isNaN = function(obj) {
974 | return _.isNumber(obj) && obj != +obj;
975 | };
976 |
977 | // Is a given value a boolean?
978 | _.isBoolean = function(obj) {
979 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
980 | };
981 |
982 | // Is a given value equal to null?
983 | _.isNull = function(obj) {
984 | return obj === null;
985 | };
986 |
987 | // Is a given variable undefined?
988 | _.isUndefined = function(obj) {
989 | return obj === void 0;
990 | };
991 |
992 | // Shortcut function for checking if an object has a given property directly
993 | // on itself (in other words, not on a prototype).
994 | _.has = function(obj, key) {
995 | return hasOwnProperty.call(obj, key);
996 | };
997 |
998 | // Utility Functions
999 | // -----------------
1000 |
1001 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
1002 | // previous owner. Returns a reference to the Underscore object.
1003 | _.noConflict = function() {
1004 | root._ = previousUnderscore;
1005 | return this;
1006 | };
1007 |
1008 | // Keep the identity function around for default iterators.
1009 | _.identity = function(value) {
1010 | return value;
1011 | };
1012 |
1013 | // Run a function **n** times.
1014 | _.times = function(n, iterator, context) {
1015 | var accum = Array(n);
1016 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
1017 | return accum;
1018 | };
1019 |
1020 | // Return a random integer between min and max (inclusive).
1021 | _.random = function(min, max) {
1022 | if (max == null) {
1023 | max = min;
1024 | min = 0;
1025 | }
1026 | return min + Math.floor(Math.random() * (max - min + 1));
1027 | };
1028 |
1029 | // List of HTML entities for escaping.
1030 | var entityMap = {
1031 | escape: {
1032 | '&': '&',
1033 | '<': '<',
1034 | '>': '>',
1035 | '"': '"',
1036 | "'": ''',
1037 | '/': '/'
1038 | }
1039 | };
1040 | entityMap.unescape = _.invert(entityMap.escape);
1041 |
1042 | // Regexes containing the keys and values listed immediately above.
1043 | var entityRegexes = {
1044 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
1045 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
1046 | };
1047 |
1048 | // Functions for escaping and unescaping strings to/from HTML interpolation.
1049 | _.each(['escape', 'unescape'], function(method) {
1050 | _[method] = function(string) {
1051 | if (string == null) return '';
1052 | return ('' + string).replace(entityRegexes[method], function(match) {
1053 | return entityMap[method][match];
1054 | });
1055 | };
1056 | });
1057 |
1058 | // If the value of the named property is a function then invoke it;
1059 | // otherwise, return it.
1060 | _.result = function(object, property) {
1061 | if (object == null) return null;
1062 | var value = object[property];
1063 | return _.isFunction(value) ? value.call(object) : value;
1064 | };
1065 |
1066 | // Add your own custom functions to the Underscore object.
1067 | _.mixin = function(obj) {
1068 | each(_.functions(obj), function(name){
1069 | var func = _[name] = obj[name];
1070 | _.prototype[name] = function() {
1071 | var args = [this._wrapped];
1072 | push.apply(args, arguments);
1073 | return result.call(this, func.apply(_, args));
1074 | };
1075 | });
1076 | };
1077 |
1078 | // Generate a unique integer id (unique within the entire client session).
1079 | // Useful for temporary DOM ids.
1080 | var idCounter = 0;
1081 | _.uniqueId = function(prefix) {
1082 | var id = ++idCounter + '';
1083 | return prefix ? prefix + id : id;
1084 | };
1085 |
1086 | // By default, Underscore uses ERB-style template delimiters, change the
1087 | // following template settings to use alternative delimiters.
1088 | _.templateSettings = {
1089 | evaluate : /<%([\s\S]+?)%>/g,
1090 | interpolate : /<%=([\s\S]+?)%>/g,
1091 | escape : /<%-([\s\S]+?)%>/g
1092 | };
1093 |
1094 | // When customizing `templateSettings`, if you don't want to define an
1095 | // interpolation, evaluation or escaping regex, we need one that is
1096 | // guaranteed not to match.
1097 | var noMatch = /(.)^/;
1098 |
1099 | // Certain characters need to be escaped so that they can be put into a
1100 | // string literal.
1101 | var escapes = {
1102 | "'": "'",
1103 | '\\': '\\',
1104 | '\r': 'r',
1105 | '\n': 'n',
1106 | '\t': 't',
1107 | '\u2028': 'u2028',
1108 | '\u2029': 'u2029'
1109 | };
1110 |
1111 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
1112 |
1113 | // JavaScript micro-templating, similar to John Resig's implementation.
1114 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
1115 | // and correctly escapes quotes within interpolated code.
1116 | _.template = function(text, data, settings) {
1117 | var render;
1118 | settings = _.defaults({}, settings, _.templateSettings);
1119 |
1120 | // Combine delimiters into one regular expression via alternation.
1121 | var matcher = new RegExp([
1122 | (settings.escape || noMatch).source,
1123 | (settings.interpolate || noMatch).source,
1124 | (settings.evaluate || noMatch).source
1125 | ].join('|') + '|$', 'g');
1126 |
1127 | // Compile the template source, escaping string literals appropriately.
1128 | var index = 0;
1129 | var source = "__p+='";
1130 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
1131 | source += text.slice(index, offset)
1132 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
1133 |
1134 | if (escape) {
1135 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
1136 | }
1137 | if (interpolate) {
1138 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
1139 | }
1140 | if (evaluate) {
1141 | source += "';\n" + evaluate + "\n__p+='";
1142 | }
1143 | index = offset + match.length;
1144 | return match;
1145 | });
1146 | source += "';\n";
1147 |
1148 | // If a variable is not specified, place data values in local scope.
1149 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
1150 |
1151 | source = "var __t,__p='',__j=Array.prototype.join," +
1152 | "print=function(){__p+=__j.call(arguments,'');};\n" +
1153 | source + "return __p;\n";
1154 |
1155 | try {
1156 | render = new Function(settings.variable || 'obj', '_', source);
1157 | } catch (e) {
1158 | e.source = source;
1159 | throw e;
1160 | }
1161 |
1162 | if (data) return render(data, _);
1163 | var template = function(data) {
1164 | return render.call(this, data, _);
1165 | };
1166 |
1167 | // Provide the compiled function source as a convenience for precompilation.
1168 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
1169 |
1170 | return template;
1171 | };
1172 |
1173 | // Add a "chain" function, which will delegate to the wrapper.
1174 | _.chain = function(obj) {
1175 | return _(obj).chain();
1176 | };
1177 |
1178 | // OOP
1179 | // ---------------
1180 | // If Underscore is called as a function, it returns a wrapped object that
1181 | // can be used OO-style. This wrapper holds altered versions of all the
1182 | // underscore functions. Wrapped objects may be chained.
1183 |
1184 | // Helper function to continue chaining intermediate results.
1185 | var result = function(obj) {
1186 | return this._chain ? _(obj).chain() : obj;
1187 | };
1188 |
1189 | // Add all of the Underscore functions to the wrapper object.
1190 | _.mixin(_);
1191 |
1192 | // Add all mutator Array functions to the wrapper.
1193 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
1194 | var method = ArrayProto[name];
1195 | _.prototype[name] = function() {
1196 | var obj = this._wrapped;
1197 | method.apply(obj, arguments);
1198 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
1199 | return result.call(this, obj);
1200 | };
1201 | });
1202 |
1203 | // Add all accessor Array functions to the wrapper.
1204 | each(['concat', 'join', 'slice'], function(name) {
1205 | var method = ArrayProto[name];
1206 | _.prototype[name] = function() {
1207 | return result.call(this, method.apply(this._wrapped, arguments));
1208 | };
1209 | });
1210 |
1211 | _.extend(_.prototype, {
1212 |
1213 | // Start chaining a wrapped Underscore object.
1214 | chain: function() {
1215 | this._chain = true;
1216 | return this;
1217 | },
1218 |
1219 | // Extracts the result from a wrapped and chained object.
1220 | value: function() {
1221 | return this._wrapped;
1222 | }
1223 |
1224 | });
1225 |
1226 | }).call(this);
1227 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/backbone.js:
--------------------------------------------------------------------------------
1 | // Backbone.js 0.9.10
2 |
3 | // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
4 | // Backbone may be freely distributed under the MIT license.
5 | // For all details and documentation:
6 | // http://backbonejs.org
7 |
8 | (function(){
9 |
10 | // Initial Setup
11 | // -------------
12 |
13 | // Save a reference to the global object (`window` in the browser, `exports`
14 | // on the server).
15 | var root = this;
16 |
17 | // Save the previous value of the `Backbone` variable, so that it can be
18 | // restored later on, if `noConflict` is used.
19 | var previousBackbone = root.Backbone;
20 |
21 | // Create a local reference to array methods.
22 | var array = [];
23 | var push = array.push;
24 | var slice = array.slice;
25 | var splice = array.splice;
26 |
27 | // The top-level namespace. All public Backbone classes and modules will
28 | // be attached to this. Exported for both CommonJS and the browser.
29 | var Backbone;
30 | if (typeof exports !== 'undefined') {
31 | Backbone = exports;
32 | } else {
33 | Backbone = root.Backbone = {};
34 | }
35 |
36 | // Current version of the library. Keep in sync with `package.json`.
37 | Backbone.VERSION = '0.9.10';
38 |
39 | // Require Underscore, if we're on the server, and it's not already present.
40 | var _ = root._;
41 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
42 |
43 | // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
44 | Backbone.$ = root.jQuery || root.Zepto || root.ender;
45 |
46 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
47 | // to its previous owner. Returns a reference to this Backbone object.
48 | Backbone.noConflict = function() {
49 | root.Backbone = previousBackbone;
50 | return this;
51 | };
52 |
53 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
54 | // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
55 | // set a `X-Http-Method-Override` header.
56 | Backbone.emulateHTTP = false;
57 |
58 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct
59 | // `application/json` requests ... will encode the body as
60 | // `application/x-www-form-urlencoded` instead and will send the model in a
61 | // form param named `model`.
62 | Backbone.emulateJSON = false;
63 |
64 | // Backbone.Events
65 | // ---------------
66 |
67 | // Regular expression used to split event strings.
68 | var eventSplitter = /\s+/;
69 |
70 | // Implement fancy features of the Events API such as multiple event
71 | // names `"change blur"` and jQuery-style event maps `{change: action}`
72 | // in terms of the existing API.
73 | var eventsApi = function(obj, action, name, rest) {
74 | if (!name) return true;
75 | if (typeof name === 'object') {
76 | for (var key in name) {
77 | obj[action].apply(obj, [key, name[key]].concat(rest));
78 | }
79 | } else if (eventSplitter.test(name)) {
80 | var names = name.split(eventSplitter);
81 | for (var i = 0, l = names.length; i < l; i++) {
82 | obj[action].apply(obj, [names[i]].concat(rest));
83 | }
84 | } else {
85 | return true;
86 | }
87 | };
88 |
89 | // Optimized internal dispatch function for triggering events. Tries to
90 | // keep the usual cases speedy (most Backbone events have 3 arguments).
91 | var triggerEvents = function(events, args) {
92 | var ev, i = -1, l = events.length;
93 | switch (args.length) {
94 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
95 | return;
96 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
97 | return;
98 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
99 | return;
100 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
101 | return;
102 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
103 | }
104 | };
105 |
106 | // A module that can be mixed in to *any object* in order to provide it with
107 | // custom events. You may bind with `on` or remove with `off` callback
108 | // functions to an event; `trigger`-ing an event fires all callbacks in
109 | // succession.
110 | //
111 | // var object = {};
112 | // _.extend(object, Backbone.Events);
113 | // object.on('expand', function(){ alert('expanded'); });
114 | // object.trigger('expand');
115 | //
116 | var Events = Backbone.Events = {
117 |
118 | // Bind one or more space separated events, or an events map,
119 | // to a `callback` function. Passing `"all"` will bind the callback to
120 | // all events fired.
121 | on: function(name, callback, context) {
122 | if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
123 | this._events || (this._events = {});
124 | var list = this._events[name] || (this._events[name] = []);
125 | list.push({callback: callback, context: context, ctx: context || this});
126 | return this;
127 | },
128 |
129 | // Bind events to only be triggered a single time. After the first time
130 | // the callback is invoked, it will be removed.
131 | once: function(name, callback, context) {
132 | if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this;
133 | var self = this;
134 | var once = _.once(function() {
135 | self.off(name, once);
136 | callback.apply(this, arguments);
137 | });
138 | once._callback = callback;
139 | this.on(name, once, context);
140 | return this;
141 | },
142 |
143 | // Remove one or many callbacks. If `context` is null, removes all
144 | // callbacks with that function. If `callback` is null, removes all
145 | // callbacks for the event. If `name` is null, removes all bound
146 | // callbacks for all events.
147 | off: function(name, callback, context) {
148 | var list, ev, events, names, i, l, j, k;
149 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
150 | if (!name && !callback && !context) {
151 | this._events = {};
152 | return this;
153 | }
154 |
155 | names = name ? [name] : _.keys(this._events);
156 | for (i = 0, l = names.length; i < l; i++) {
157 | name = names[i];
158 | if (list = this._events[name]) {
159 | events = [];
160 | if (callback || context) {
161 | for (j = 0, k = list.length; j < k; j++) {
162 | ev = list[j];
163 | if ((callback && callback !== ev.callback &&
164 | callback !== ev.callback._callback) ||
165 | (context && context !== ev.context)) {
166 | events.push(ev);
167 | }
168 | }
169 | }
170 | this._events[name] = events;
171 | }
172 | }
173 |
174 | return this;
175 | },
176 |
177 | // Trigger one or many events, firing all bound callbacks. Callbacks are
178 | // passed the same arguments as `trigger` is, apart from the event name
179 | // (unless you're listening on `"all"`, which will cause your callback to
180 | // receive the true name of the event as the first argument).
181 | trigger: function(name) {
182 | if (!this._events) return this;
183 | var args = slice.call(arguments, 1);
184 | if (!eventsApi(this, 'trigger', name, args)) return this;
185 | var events = this._events[name];
186 | var allEvents = this._events.all;
187 | if (events) triggerEvents(events, args);
188 | if (allEvents) triggerEvents(allEvents, arguments);
189 | return this;
190 | },
191 |
192 | // An inversion-of-control version of `on`. Tell *this* object to listen to
193 | // an event in another object ... keeping track of what it's listening to.
194 | listenTo: function(obj, name, callback) {
195 | var listeners = this._listeners || (this._listeners = {});
196 | var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
197 | listeners[id] = obj;
198 | obj.on(name, typeof name === 'object' ? this : callback, this);
199 | return this;
200 | },
201 |
202 | // Tell this object to stop listening to either specific events ... or
203 | // to every object it's currently listening to.
204 | stopListening: function(obj, name, callback) {
205 | var listeners = this._listeners;
206 | if (!listeners) return;
207 | if (obj) {
208 | obj.off(name, typeof name === 'object' ? this : callback, this);
209 | if (!name && !callback) delete listeners[obj._listenerId];
210 | } else {
211 | if (typeof name === 'object') callback = this;
212 | for (var id in listeners) {
213 | listeners[id].off(name, callback, this);
214 | }
215 | this._listeners = {};
216 | }
217 | return this;
218 | }
219 | };
220 |
221 | // Aliases for backwards compatibility.
222 | Events.bind = Events.on;
223 | Events.unbind = Events.off;
224 |
225 | // Allow the `Backbone` object to serve as a global event bus, for folks who
226 | // want global "pubsub" in a convenient place.
227 | _.extend(Backbone, Events);
228 |
229 | // Backbone.Model
230 | // --------------
231 |
232 | // Create a new model, with defined attributes. A client id (`cid`)
233 | // is automatically generated and assigned for you.
234 | var Model = Backbone.Model = function(attributes, options) {
235 | var defaults;
236 | var attrs = attributes || {};
237 | this.cid = _.uniqueId('c');
238 | this.attributes = {};
239 | if (options && options.collection) this.collection = options.collection;
240 | if (options && options.parse) attrs = this.parse(attrs, options) || {};
241 | if (defaults = _.result(this, 'defaults')) {
242 | attrs = _.defaults({}, attrs, defaults);
243 | }
244 | this.set(attrs, options);
245 | this.changed = {};
246 | this.initialize.apply(this, arguments);
247 | };
248 |
249 | // Attach all inheritable methods to the Model prototype.
250 | _.extend(Model.prototype, Events, {
251 |
252 | // A hash of attributes whose current and previous value differ.
253 | changed: null,
254 |
255 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and
256 | // CouchDB users may want to set this to `"_id"`.
257 | idAttribute: 'id',
258 |
259 | // Initialize is an empty function by default. Override it with your own
260 | // initialization logic.
261 | initialize: function(){},
262 |
263 | // Return a copy of the model's `attributes` object.
264 | toJSON: function(options) {
265 | return _.clone(this.attributes);
266 | },
267 |
268 | // Proxy `Backbone.sync` by default.
269 | sync: function() {
270 | return Backbone.sync.apply(this, arguments);
271 | },
272 |
273 | // Get the value of an attribute.
274 | get: function(attr) {
275 | return this.attributes[attr];
276 | },
277 |
278 | // Get the HTML-escaped value of an attribute.
279 | escape: function(attr) {
280 | return _.escape(this.get(attr));
281 | },
282 |
283 | // Returns `true` if the attribute contains a value that is not null
284 | // or undefined.
285 | has: function(attr) {
286 | return this.get(attr) != null;
287 | },
288 |
289 | // ----------------------------------------------------------------------
290 |
291 | // Set a hash of model attributes on the object, firing `"change"` unless
292 | // you choose to silence it.
293 | set: function(key, val, options) {
294 | var attr, attrs, unset, changes, silent, changing, prev, current;
295 | if (key == null) return this;
296 |
297 | // Handle both `"key", value` and `{key: value}` -style arguments.
298 | if (typeof key === 'object') {
299 | attrs = key;
300 | options = val;
301 | } else {
302 | (attrs = {})[key] = val;
303 | }
304 |
305 | options || (options = {});
306 |
307 | // Run validation.
308 | if (!this._validate(attrs, options)) return false;
309 |
310 | // Extract attributes and options.
311 | unset = options.unset;
312 | silent = options.silent;
313 | changes = [];
314 | changing = this._changing;
315 | this._changing = true;
316 |
317 | if (!changing) {
318 | this._previousAttributes = _.clone(this.attributes);
319 | this.changed = {};
320 | }
321 | current = this.attributes, prev = this._previousAttributes;
322 |
323 | // Check for changes of `id`.
324 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
325 |
326 | // For each `set` attribute, update or delete the current value.
327 | for (attr in attrs) {
328 | val = attrs[attr];
329 | if (!_.isEqual(current[attr], val)) changes.push(attr);
330 | if (!_.isEqual(prev[attr], val)) {
331 | this.changed[attr] = val;
332 | } else {
333 | delete this.changed[attr];
334 | }
335 | unset ? delete current[attr] : current[attr] = val;
336 | }
337 |
338 | // Trigger all relevant attribute changes.
339 | if (!silent) {
340 | if (changes.length) this._pending = true;
341 | for (var i = 0, l = changes.length; i < l; i++) {
342 | this.trigger('change:' + changes[i], this, current[changes[i]], options);
343 | }
344 | }
345 |
346 | if (changing) return this;
347 | if (!silent) {
348 | while (this._pending) {
349 | this._pending = false;
350 | this.trigger('change', this, options);
351 | }
352 | }
353 | this._pending = false;
354 | this._changing = false;
355 | return this;
356 | },
357 |
358 | // Remove an attribute from the model, firing `"change"` unless you choose
359 | // to silence it. `unset` is a noop if the attribute doesn't exist.
360 | unset: function(attr, options) {
361 | return this.set(attr, void 0, _.extend({}, options, {unset: true}));
362 | },
363 |
364 | // Clear all attributes on the model, firing `"change"` unless you choose
365 | // to silence it.
366 | clear: function(options) {
367 | var attrs = {};
368 | for (var key in this.attributes) attrs[key] = void 0;
369 | return this.set(attrs, _.extend({}, options, {unset: true}));
370 | },
371 |
372 | // Determine if the model has changed since the last `"change"` event.
373 | // If you specify an attribute name, determine if that attribute has changed.
374 | hasChanged: function(attr) {
375 | if (attr == null) return !_.isEmpty(this.changed);
376 | return _.has(this.changed, attr);
377 | },
378 |
379 | // Return an object containing all the attributes that have changed, or
380 | // false if there are no changed attributes. Useful for determining what
381 | // parts of a view need to be updated and/or what attributes need to be
382 | // persisted to the server. Unset attributes will be set to undefined.
383 | // You can also pass an attributes object to diff against the model,
384 | // determining if there *would be* a change.
385 | changedAttributes: function(diff) {
386 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
387 | var val, changed = false;
388 | var old = this._changing ? this._previousAttributes : this.attributes;
389 | for (var attr in diff) {
390 | if (_.isEqual(old[attr], (val = diff[attr]))) continue;
391 | (changed || (changed = {}))[attr] = val;
392 | }
393 | return changed;
394 | },
395 |
396 | // Get the previous value of an attribute, recorded at the time the last
397 | // `"change"` event was fired.
398 | previous: function(attr) {
399 | if (attr == null || !this._previousAttributes) return null;
400 | return this._previousAttributes[attr];
401 | },
402 |
403 | // Get all of the attributes of the model at the time of the previous
404 | // `"change"` event.
405 | previousAttributes: function() {
406 | return _.clone(this._previousAttributes);
407 | },
408 |
409 | // ---------------------------------------------------------------------
410 |
411 | // Fetch the model from the server. If the server's representation of the
412 | // model differs from its current attributes, they will be overriden,
413 | // triggering a `"change"` event.
414 | fetch: function(options) {
415 | options = options ? _.clone(options) : {};
416 | if (options.parse === void 0) options.parse = true;
417 | var success = options.success;
418 | options.success = function(model, resp, options) {
419 | if (!model.set(model.parse(resp, options), options)) return false;
420 | if (success) success(model, resp, options);
421 | };
422 | return this.sync('read', this, options);
423 | },
424 |
425 | // Set a hash of model attributes, and sync the model to the server.
426 | // If the server returns an attributes hash that differs, the model's
427 | // state will be `set` again.
428 | save: function(key, val, options) {
429 | var attrs, success, method, xhr, attributes = this.attributes;
430 |
431 | // Handle both `"key", value` and `{key: value}` -style arguments.
432 | if (key == null || typeof key === 'object') {
433 | attrs = key;
434 | options = val;
435 | } else {
436 | (attrs = {})[key] = val;
437 | }
438 |
439 | // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
440 | if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
441 |
442 | options = _.extend({validate: true}, options);
443 |
444 | // Do not persist invalid models.
445 | if (!this._validate(attrs, options)) return false;
446 |
447 | // Set temporary attributes if `{wait: true}`.
448 | if (attrs && options.wait) {
449 | this.attributes = _.extend({}, attributes, attrs);
450 | }
451 |
452 | // After a successful server-side save, the client is (optionally)
453 | // updated with the server-side state.
454 | if (options.parse === void 0) options.parse = true;
455 | success = options.success;
456 | options.success = function(model, resp, options) {
457 | // Ensure attributes are restored during synchronous saves.
458 | model.attributes = attributes;
459 | var serverAttrs = model.parse(resp, options);
460 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
461 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
462 | return false;
463 | }
464 | if (success) success(model, resp, options);
465 | };
466 |
467 | // Finish configuring and sending the Ajax request.
468 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
469 | if (method === 'patch') options.attrs = attrs;
470 | xhr = this.sync(method, this, options);
471 |
472 | // Restore attributes.
473 | if (attrs && options.wait) this.attributes = attributes;
474 |
475 | return xhr;
476 | },
477 |
478 | // Destroy this model on the server if it was already persisted.
479 | // Optimistically removes the model from its collection, if it has one.
480 | // If `wait: true` is passed, waits for the server to respond before removal.
481 | destroy: function(options) {
482 | options = options ? _.clone(options) : {};
483 | var model = this;
484 | var success = options.success;
485 |
486 | var destroy = function() {
487 | model.trigger('destroy', model, model.collection, options);
488 | };
489 |
490 | options.success = function(model, resp, options) {
491 | if (options.wait || model.isNew()) destroy();
492 | if (success) success(model, resp, options);
493 | };
494 |
495 | if (this.isNew()) {
496 | options.success(this, null, options);
497 | return false;
498 | }
499 |
500 | var xhr = this.sync('delete', this, options);
501 | if (!options.wait) destroy();
502 | return xhr;
503 | },
504 |
505 | // Default URL for the model's representation on the server -- if you're
506 | // using Backbone's restful methods, override this to change the endpoint
507 | // that will be called.
508 | url: function() {
509 | var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
510 | if (this.isNew()) return base;
511 | return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
512 | },
513 |
514 | // **parse** converts a response into the hash of attributes to be `set` on
515 | // the model. The default implementation is just to pass the response along.
516 | parse: function(resp, options) {
517 | return resp;
518 | },
519 |
520 | // Create a new model with identical attributes to this one.
521 | clone: function() {
522 | return new this.constructor(this.attributes);
523 | },
524 |
525 | // A model is new if it has never been saved to the server, and lacks an id.
526 | isNew: function() {
527 | return this.id == null;
528 | },
529 |
530 | // Check if the model is currently in a valid state.
531 | isValid: function(options) {
532 | return !this.validate || !this.validate(this.attributes, options);
533 | },
534 |
535 | // Run validation against the next complete set of model attributes,
536 | // returning `true` if all is well. Otherwise, fire a general
537 | // `"error"` event and call the error callback, if specified.
538 | _validate: function(attrs, options) {
539 | if (!options.validate || !this.validate) return true;
540 | attrs = _.extend({}, this.attributes, attrs);
541 | var error = this.validationError = this.validate(attrs, options) || null;
542 | if (!error) return true;
543 | this.trigger('invalid', this, error, options || {});
544 | return false;
545 | }
546 |
547 | });
548 |
549 | // Backbone.Collection
550 | // -------------------
551 |
552 | // Provides a standard collection class for our sets of models, ordered
553 | // or unordered. If a `comparator` is specified, the Collection will maintain
554 | // its models in sort order, as they're added and removed.
555 | var Collection = Backbone.Collection = function(models, options) {
556 | options || (options = {});
557 | if (options.model) this.model = options.model;
558 | if (options.comparator !== void 0) this.comparator = options.comparator;
559 | this.models = [];
560 | this._reset();
561 | this.initialize.apply(this, arguments);
562 | if (models) this.reset(models, _.extend({silent: true}, options));
563 | };
564 |
565 | // Define the Collection's inheritable methods.
566 | _.extend(Collection.prototype, Events, {
567 |
568 | // The default model for a collection is just a **Backbone.Model**.
569 | // This should be overridden in most cases.
570 | model: Model,
571 |
572 | // Initialize is an empty function by default. Override it with your own
573 | // initialization logic.
574 | initialize: function(){},
575 |
576 | // The JSON representation of a Collection is an array of the
577 | // models' attributes.
578 | toJSON: function(options) {
579 | return this.map(function(model){ return model.toJSON(options); });
580 | },
581 |
582 | // Proxy `Backbone.sync` by default.
583 | sync: function() {
584 | return Backbone.sync.apply(this, arguments);
585 | },
586 |
587 | // Add a model, or list of models to the set.
588 | add: function(models, options) {
589 | models = _.isArray(models) ? models.slice() : [models];
590 | options || (options = {});
591 | var i, l, model, attrs, existing, doSort, add, at, sort, sortAttr;
592 | add = [];
593 | at = options.at;
594 | sort = this.comparator && (at == null) && options.sort != false;
595 | sortAttr = _.isString(this.comparator) ? this.comparator : null;
596 |
597 | // Turn bare objects into model references, and prevent invalid models
598 | // from being added.
599 | for (i = 0, l = models.length; i < l; i++) {
600 | if (!(model = this._prepareModel(attrs = models[i], options))) {
601 | this.trigger('invalid', this, attrs, options);
602 | continue;
603 | }
604 |
605 | // If a duplicate is found, prevent it from being added and
606 | // optionally merge it into the existing model.
607 | if (existing = this.get(model)) {
608 | if (options.merge) {
609 | existing.set(attrs === model ? model.attributes : attrs, options);
610 | if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true;
611 | }
612 | continue;
613 | }
614 |
615 | // This is a new model, push it to the `add` list.
616 | add.push(model);
617 |
618 | // Listen to added models' events, and index models for lookup by
619 | // `id` and by `cid`.
620 | model.on('all', this._onModelEvent, this);
621 | this._byId[model.cid] = model;
622 | if (model.id != null) this._byId[model.id] = model;
623 | }
624 |
625 | // See if sorting is needed, update `length` and splice in new models.
626 | if (add.length) {
627 | if (sort) doSort = true;
628 | this.length += add.length;
629 | if (at != null) {
630 | splice.apply(this.models, [at, 0].concat(add));
631 | } else {
632 | push.apply(this.models, add);
633 | }
634 | }
635 |
636 | // Silently sort the collection if appropriate.
637 | if (doSort) this.sort({silent: true});
638 |
639 | if (options.silent) return this;
640 |
641 | // Trigger `add` events.
642 | for (i = 0, l = add.length; i < l; i++) {
643 | (model = add[i]).trigger('add', model, this, options);
644 | }
645 |
646 | // Trigger `sort` if the collection was sorted.
647 | if (doSort) this.trigger('sort', this, options);
648 |
649 | return this;
650 | },
651 |
652 | // Remove a model, or a list of models from the set.
653 | remove: function(models, options) {
654 | models = _.isArray(models) ? models.slice() : [models];
655 | options || (options = {});
656 | var i, l, index, model;
657 | for (i = 0, l = models.length; i < l; i++) {
658 | model = this.get(models[i]);
659 | if (!model) continue;
660 | delete this._byId[model.id];
661 | delete this._byId[model.cid];
662 | index = this.indexOf(model);
663 | this.models.splice(index, 1);
664 | this.length--;
665 | if (!options.silent) {
666 | options.index = index;
667 | model.trigger('remove', model, this, options);
668 | }
669 | this._removeReference(model);
670 | }
671 | return this;
672 | },
673 |
674 | // Add a model to the end of the collection.
675 | push: function(model, options) {
676 | model = this._prepareModel(model, options);
677 | this.add(model, _.extend({at: this.length}, options));
678 | return model;
679 | },
680 |
681 | // Remove a model from the end of the collection.
682 | pop: function(options) {
683 | var model = this.at(this.length - 1);
684 | this.remove(model, options);
685 | return model;
686 | },
687 |
688 | // Add a model to the beginning of the collection.
689 | unshift: function(model, options) {
690 | model = this._prepareModel(model, options);
691 | this.add(model, _.extend({at: 0}, options));
692 | return model;
693 | },
694 |
695 | // Remove a model from the beginning of the collection.
696 | shift: function(options) {
697 | var model = this.at(0);
698 | this.remove(model, options);
699 | return model;
700 | },
701 |
702 | // Slice out a sub-array of models from the collection.
703 | slice: function(begin, end) {
704 | return this.models.slice(begin, end);
705 | },
706 |
707 | // Get a model from the set by id.
708 | get: function(obj) {
709 | if (obj == null) return void 0;
710 | this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
711 | return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
712 | },
713 |
714 | // Get the model at the given index.
715 | at: function(index) {
716 | return this.models[index];
717 | },
718 |
719 | // Return models with matching attributes. Useful for simple cases of `filter`.
720 | where: function(attrs) {
721 | if (_.isEmpty(attrs)) return [];
722 | return this.filter(function(model) {
723 | for (var key in attrs) {
724 | if (attrs[key] !== model.get(key)) return false;
725 | }
726 | return true;
727 | });
728 | },
729 |
730 | // Force the collection to re-sort itself. You don't need to call this under
731 | // normal circumstances, as the set will maintain sort order as each item
732 | // is added.
733 | sort: function(options) {
734 | if (!this.comparator) {
735 | throw new Error('Cannot sort a set without a comparator');
736 | }
737 | options || (options = {});
738 |
739 | // Run sort based on type of `comparator`.
740 | if (_.isString(this.comparator) || this.comparator.length === 1) {
741 | this.models = this.sortBy(this.comparator, this);
742 | } else {
743 | this.models.sort(_.bind(this.comparator, this));
744 | }
745 |
746 | if (!options.silent) this.trigger('sort', this, options);
747 | return this;
748 | },
749 |
750 | // Pluck an attribute from each model in the collection.
751 | pluck: function(attr) {
752 | return _.invoke(this.models, 'get', attr);
753 | },
754 |
755 | // Smartly update a collection with a change set of models, adding,
756 | // removing, and merging as necessary.
757 | update: function(models, options) {
758 | options = _.extend({add: true, merge: true, remove: true}, options);
759 | if (options.parse) models = this.parse(models, options);
760 | var model, i, l, existing;
761 | var add = [], remove = [], modelMap = {};
762 |
763 | // Allow a single model (or no argument) to be passed.
764 | if (!_.isArray(models)) models = models ? [models] : [];
765 |
766 | // Proxy to `add` for this case, no need to iterate...
767 | if (options.add && !options.remove) return this.add(models, options);
768 |
769 | // Determine which models to add and merge, and which to remove.
770 | for (i = 0, l = models.length; i < l; i++) {
771 | model = models[i];
772 | existing = this.get(model);
773 | if (options.remove && existing) modelMap[existing.cid] = true;
774 | if ((options.add && !existing) || (options.merge && existing)) {
775 | add.push(model);
776 | }
777 | }
778 | if (options.remove) {
779 | for (i = 0, l = this.models.length; i < l; i++) {
780 | model = this.models[i];
781 | if (!modelMap[model.cid]) remove.push(model);
782 | }
783 | }
784 |
785 | // Remove models (if applicable) before we add and merge the rest.
786 | if (remove.length) this.remove(remove, options);
787 | if (add.length) this.add(add, options);
788 | return this;
789 | },
790 |
791 | // When you have more items than you want to add or remove individually,
792 | // you can reset the entire set with a new list of models, without firing
793 | // any `add` or `remove` events. Fires `reset` when finished.
794 | reset: function(models, options) {
795 | options || (options = {});
796 | if (options.parse) models = this.parse(models, options);
797 | for (var i = 0, l = this.models.length; i < l; i++) {
798 | this._removeReference(this.models[i]);
799 | }
800 | options.previousModels = this.models.slice();
801 | this._reset();
802 | if (models) this.add(models, _.extend({silent: true}, options));
803 | if (!options.silent) this.trigger('reset', this, options);
804 | return this;
805 | },
806 |
807 | // Fetch the default set of models for this collection, resetting the
808 | // collection when they arrive. If `update: true` is passed, the response
809 | // data will be passed through the `update` method instead of `reset`.
810 | fetch: function(options) {
811 | options = options ? _.clone(options) : {};
812 | if (options.parse === void 0) options.parse = true;
813 | var success = options.success;
814 | options.success = function(collection, resp, options) {
815 | var method = options.update ? 'update' : 'reset';
816 | collection[method](resp, options);
817 | if (success) success(collection, resp, options);
818 | };
819 | return this.sync('read', this, options);
820 | },
821 |
822 | // Create a new instance of a model in this collection. Add the model to the
823 | // collection immediately, unless `wait: true` is passed, in which case we
824 | // wait for the server to agree.
825 | create: function(model, options) {
826 | options = options ? _.clone(options) : {};
827 | if (!(model = this._prepareModel(model, options))) return false;
828 | if (!options.wait) this.add(model, options);
829 | var collection = this;
830 | var success = options.success;
831 | options.success = function(model, resp, options) {
832 | if (options.wait) collection.add(model, options);
833 | if (success) success(model, resp, options);
834 | };
835 | model.save(null, options);
836 | return model;
837 | },
838 |
839 | // **parse** converts a response into a list of models to be added to the
840 | // collection. The default implementation is just to pass it through.
841 | parse: function(resp, options) {
842 | return resp;
843 | },
844 |
845 | // Create a new collection with an identical list of models as this one.
846 | clone: function() {
847 | return new this.constructor(this.models);
848 | },
849 |
850 | // Reset all internal state. Called when the collection is reset.
851 | _reset: function() {
852 | this.length = 0;
853 | this.models.length = 0;
854 | this._byId = {};
855 | },
856 |
857 | // Prepare a model or hash of attributes to be added to this collection.
858 | _prepareModel: function(attrs, options) {
859 | if (attrs instanceof Model) {
860 | if (!attrs.collection) attrs.collection = this;
861 | return attrs;
862 | }
863 | options || (options = {});
864 | options.collection = this;
865 | var model = new this.model(attrs, options);
866 | if (!model._validate(attrs, options)) return false;
867 | return model;
868 | },
869 |
870 | // Internal method to remove a model's ties to a collection.
871 | _removeReference: function(model) {
872 | if (this === model.collection) delete model.collection;
873 | model.off('all', this._onModelEvent, this);
874 | },
875 |
876 | // Internal method called every time a model in the set fires an event.
877 | // Sets need to update their indexes when models change ids. All other
878 | // events simply proxy through. "add" and "remove" events that originate
879 | // in other collections are ignored.
880 | _onModelEvent: function(event, model, collection, options) {
881 | if ((event === 'add' || event === 'remove') && collection !== this) return;
882 | if (event === 'destroy') this.remove(model, options);
883 | if (model && event === 'change:' + model.idAttribute) {
884 | delete this._byId[model.previous(model.idAttribute)];
885 | if (model.id != null) this._byId[model.id] = model;
886 | }
887 | this.trigger.apply(this, arguments);
888 | },
889 |
890 | sortedIndex: function (model, value, context) {
891 | value || (value = this.comparator);
892 | var iterator = _.isFunction(value) ? value : function(model) {
893 | return model.get(value);
894 | };
895 | return _.sortedIndex(this.models, model, iterator, context);
896 | }
897 |
898 | });
899 |
900 | // Underscore methods that we want to implement on the Collection.
901 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
902 | 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
903 | 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
904 | 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
905 | 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
906 | 'isEmpty', 'chain'];
907 |
908 | // Mix in each Underscore method as a proxy to `Collection#models`.
909 | _.each(methods, function(method) {
910 | Collection.prototype[method] = function() {
911 | var args = slice.call(arguments);
912 | args.unshift(this.models);
913 | return _[method].apply(_, args);
914 | };
915 | });
916 |
917 | // Underscore methods that take a property name as an argument.
918 | var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
919 |
920 | // Use attributes instead of properties.
921 | _.each(attributeMethods, function(method) {
922 | Collection.prototype[method] = function(value, context) {
923 | var iterator = _.isFunction(value) ? value : function(model) {
924 | return model.get(value);
925 | };
926 | return _[method](this.models, iterator, context);
927 | };
928 | });
929 |
930 | // Backbone.Router
931 | // ---------------
932 |
933 | // Routers map faux-URLs to actions, and fire events when routes are
934 | // matched. Creating a new one sets its `routes` hash, if not set statically.
935 | var Router = Backbone.Router = function(options) {
936 | options || (options = {});
937 | if (options.routes) this.routes = options.routes;
938 | this._bindRoutes();
939 | this.initialize.apply(this, arguments);
940 | };
941 |
942 | // Cached regular expressions for matching named param parts and splatted
943 | // parts of route strings.
944 | var optionalParam = /\((.*?)\)/g;
945 | var namedParam = /(\(\?)?:\w+/g;
946 | var splatParam = /\*\w+/g;
947 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
948 |
949 | // Set up all inheritable **Backbone.Router** properties and methods.
950 | _.extend(Router.prototype, Events, {
951 |
952 | // Initialize is an empty function by default. Override it with your own
953 | // initialization logic.
954 | initialize: function(){},
955 |
956 | // Manually bind a single named route to a callback. For example:
957 | //
958 | // this.route('search/:query/p:num', 'search', function(query, num) {
959 | // ...
960 | // });
961 | //
962 | route: function(route, name, callback) {
963 | if (!_.isRegExp(route)) route = this._routeToRegExp(route);
964 | if (!callback) callback = this[name];
965 | Backbone.history.route(route, _.bind(function(fragment) {
966 | var args = this._extractParameters(route, fragment);
967 | callback && callback.apply(this, args);
968 | this.trigger.apply(this, ['route:' + name].concat(args));
969 | this.trigger('route', name, args);
970 | Backbone.history.trigger('route', this, name, args);
971 | }, this));
972 | return this;
973 | },
974 |
975 | // Simple proxy to `Backbone.history` to save a fragment into the history.
976 | navigate: function(fragment, options) {
977 | Backbone.history.navigate(fragment, options);
978 | return this;
979 | },
980 |
981 | // Bind all defined routes to `Backbone.history`. We have to reverse the
982 | // order of the routes here to support behavior where the most general
983 | // routes can be defined at the bottom of the route map.
984 | _bindRoutes: function() {
985 | if (!this.routes) return;
986 | var route, routes = _.keys(this.routes);
987 | while ((route = routes.pop()) != null) {
988 | this.route(route, this.routes[route]);
989 | }
990 | },
991 |
992 | // Convert a route string into a regular expression, suitable for matching
993 | // against the current location hash.
994 | _routeToRegExp: function(route) {
995 | route = route.replace(escapeRegExp, '\\$&')
996 | .replace(optionalParam, '(?:$1)?')
997 | .replace(namedParam, function(match, optional){
998 | return optional ? match : '([^\/]+)';
999 | })
1000 | .replace(splatParam, '(.*?)');
1001 | return new RegExp('^' + route + '$');
1002 | },
1003 |
1004 | // Given a route, and a URL fragment that it matches, return the array of
1005 | // extracted parameters.
1006 | _extractParameters: function(route, fragment) {
1007 | return route.exec(fragment).slice(1);
1008 | }
1009 |
1010 | });
1011 |
1012 | // Backbone.History
1013 | // ----------------
1014 |
1015 | // Handles cross-browser history management, based on URL fragments. If the
1016 | // browser does not support `onhashchange`, falls back to polling.
1017 | var History = Backbone.History = function() {
1018 | this.handlers = [];
1019 | _.bindAll(this, 'checkUrl');
1020 |
1021 | // Ensure that `History` can be used outside of the browser.
1022 | if (typeof window !== 'undefined') {
1023 | this.location = window.location;
1024 | this.history = window.history;
1025 | }
1026 | };
1027 |
1028 | // Cached regex for stripping a leading hash/slash and trailing space.
1029 | var routeStripper = /^[#\/]|\s+$/g;
1030 |
1031 | // Cached regex for stripping leading and trailing slashes.
1032 | var rootStripper = /^\/+|\/+$/g;
1033 |
1034 | // Cached regex for detecting MSIE.
1035 | var isExplorer = /msie [\w.]+/;
1036 |
1037 | // Cached regex for removing a trailing slash.
1038 | var trailingSlash = /\/$/;
1039 |
1040 | // Has the history handling already been started?
1041 | History.started = false;
1042 |
1043 | // Set up all inheritable **Backbone.History** properties and methods.
1044 | _.extend(History.prototype, Events, {
1045 |
1046 | // The default interval to poll for hash changes, if necessary, is
1047 | // twenty times a second.
1048 | interval: 50,
1049 |
1050 | // Gets the true hash value. Cannot use location.hash directly due to bug
1051 | // in Firefox where location.hash will always be decoded.
1052 | getHash: function(window) {
1053 | var match = (window || this).location.href.match(/#(.*)$/);
1054 | return match ? match[1] : '';
1055 | },
1056 |
1057 | // Get the cross-browser normalized URL fragment, either from the URL,
1058 | // the hash, or the override.
1059 | getFragment: function(fragment, forcePushState) {
1060 | if (fragment == null) {
1061 | if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1062 | fragment = this.location.pathname;
1063 | var root = this.root.replace(trailingSlash, '');
1064 | if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
1065 | } else {
1066 | fragment = this.getHash();
1067 | }
1068 | }
1069 | return fragment.replace(routeStripper, '');
1070 | },
1071 |
1072 | // Start the hash change handling, returning `true` if the current URL matches
1073 | // an existing route, and `false` otherwise.
1074 | start: function(options) {
1075 | if (History.started) throw new Error("Backbone.history has already been started");
1076 | History.started = true;
1077 |
1078 | // Figure out the initial configuration. Do we need an iframe?
1079 | // Is pushState desired ... is it available?
1080 | this.options = _.extend({}, {root: '/'}, this.options, options);
1081 | this.root = this.options.root;
1082 | this._wantsHashChange = this.options.hashChange !== false;
1083 | this._wantsPushState = !!this.options.pushState;
1084 | this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
1085 | var fragment = this.getFragment();
1086 | var docMode = document.documentMode;
1087 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1088 |
1089 | // Normalize root to always include a leading and trailing slash.
1090 | this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1091 |
1092 | if (oldIE && this._wantsHashChange) {
1093 | this.iframe = Backbone.$('').hide().appendTo('body')[0].contentWindow;
1094 | this.navigate(fragment);
1095 | }
1096 |
1097 | // Depending on whether we're using pushState or hashes, and whether
1098 | // 'onhashchange' is supported, determine how we check the URL state.
1099 | if (this._hasPushState) {
1100 | Backbone.$(window).on('popstate', this.checkUrl);
1101 | } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1102 | Backbone.$(window).on('hashchange', this.checkUrl);
1103 | } else if (this._wantsHashChange) {
1104 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1105 | }
1106 |
1107 | // Determine if we need to change the base url, for a pushState link
1108 | // opened by a non-pushState browser.
1109 | this.fragment = fragment;
1110 | var loc = this.location;
1111 | var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1112 |
1113 | // If we've started off with a route from a `pushState`-enabled browser,
1114 | // but we're currently in a browser that doesn't support it...
1115 | if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1116 | this.fragment = this.getFragment(null, true);
1117 | this.location.replace(this.root + this.location.search + '#' + this.fragment);
1118 | // Return immediately as browser will do redirect to new url
1119 | return true;
1120 |
1121 | // Or if we've started out with a hash-based route, but we're currently
1122 | // in a browser where it could be `pushState`-based instead...
1123 | } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1124 | this.fragment = this.getHash().replace(routeStripper, '');
1125 | this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1126 | }
1127 |
1128 | if (!this.options.silent) return this.loadUrl();
1129 | },
1130 |
1131 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1132 | // but possibly useful for unit testing Routers.
1133 | stop: function() {
1134 | Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1135 | clearInterval(this._checkUrlInterval);
1136 | History.started = false;
1137 | },
1138 |
1139 | // Add a route to be tested when the fragment changes. Routes added later
1140 | // may override previous routes.
1141 | route: function(route, callback) {
1142 | this.handlers.unshift({route: route, callback: callback});
1143 | },
1144 |
1145 | // Checks the current URL to see if it has changed, and if it has,
1146 | // calls `loadUrl`, normalizing across the hidden iframe.
1147 | checkUrl: function(e) {
1148 | var current = this.getFragment();
1149 | if (current === this.fragment && this.iframe) {
1150 | current = this.getFragment(this.getHash(this.iframe));
1151 | }
1152 | if (current === this.fragment) return false;
1153 | if (this.iframe) this.navigate(current);
1154 | this.loadUrl() || this.loadUrl(this.getHash());
1155 | },
1156 |
1157 | // Attempt to load the current URL fragment. If a route succeeds with a
1158 | // match, returns `true`. If no defined routes matches the fragment,
1159 | // returns `false`.
1160 | loadUrl: function(fragmentOverride) {
1161 | var fragment = this.fragment = this.getFragment(fragmentOverride);
1162 | var matched = _.any(this.handlers, function(handler) {
1163 | if (handler.route.test(fragment)) {
1164 | handler.callback(fragment);
1165 | return true;
1166 | }
1167 | });
1168 | return matched;
1169 | },
1170 |
1171 | // Save a fragment into the hash history, or replace the URL state if the
1172 | // 'replace' option is passed. You are responsible for properly URL-encoding
1173 | // the fragment in advance.
1174 | //
1175 | // The options object can contain `trigger: true` if you wish to have the
1176 | // route callback be fired (not usually desirable), or `replace: true`, if
1177 | // you wish to modify the current URL without adding an entry to the history.
1178 | navigate: function(fragment, options) {
1179 | if (!History.started) return false;
1180 | if (!options || options === true) options = {trigger: options};
1181 | fragment = this.getFragment(fragment || '');
1182 | if (this.fragment === fragment) return;
1183 | this.fragment = fragment;
1184 | var url = this.root + fragment;
1185 |
1186 | // If pushState is available, we use it to set the fragment as a real URL.
1187 | if (this._hasPushState) {
1188 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1189 |
1190 | // If hash changes haven't been explicitly disabled, update the hash
1191 | // fragment to store history.
1192 | } else if (this._wantsHashChange) {
1193 | this._updateHash(this.location, fragment, options.replace);
1194 | if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
1195 | // Opening and closing the iframe tricks IE7 and earlier to push a
1196 | // history entry on hash-tag change. When replace is true, we don't
1197 | // want this.
1198 | if(!options.replace) this.iframe.document.open().close();
1199 | this._updateHash(this.iframe.location, fragment, options.replace);
1200 | }
1201 |
1202 | // If you've told us that you explicitly don't want fallback hashchange-
1203 | // based history, then `navigate` becomes a page refresh.
1204 | } else {
1205 | return this.location.assign(url);
1206 | }
1207 | if (options.trigger) this.loadUrl(fragment);
1208 | },
1209 |
1210 | // Update the hash location, either replacing the current entry, or adding
1211 | // a new one to the browser history.
1212 | _updateHash: function(location, fragment, replace) {
1213 | if (replace) {
1214 | var href = location.href.replace(/(javascript:|#).*$/, '');
1215 | location.replace(href + '#' + fragment);
1216 | } else {
1217 | // Some browsers require that `hash` contains a leading #.
1218 | location.hash = '#' + fragment;
1219 | }
1220 | }
1221 |
1222 | });
1223 |
1224 | // Create the default Backbone.history.
1225 | Backbone.history = new History;
1226 |
1227 | // Backbone.View
1228 | // -------------
1229 |
1230 | // Creating a Backbone.View creates its initial element outside of the DOM,
1231 | // if an existing element is not provided...
1232 | var View = Backbone.View = function(options) {
1233 | this.cid = _.uniqueId('view');
1234 | this._configure(options || {});
1235 | this._ensureElement();
1236 | this.initialize.apply(this, arguments);
1237 | this.delegateEvents();
1238 | };
1239 |
1240 | // Cached regex to split keys for `delegate`.
1241 | var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1242 |
1243 | // List of view options to be merged as properties.
1244 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
1245 |
1246 | // Set up all inheritable **Backbone.View** properties and methods.
1247 | _.extend(View.prototype, Events, {
1248 |
1249 | // The default `tagName` of a View's element is `"div"`.
1250 | tagName: 'div',
1251 |
1252 | // jQuery delegate for element lookup, scoped to DOM elements within the
1253 | // current view. This should be prefered to global lookups where possible.
1254 | $: function(selector) {
1255 | return this.$el.find(selector);
1256 | },
1257 |
1258 | // Initialize is an empty function by default. Override it with your own
1259 | // initialization logic.
1260 | initialize: function(){},
1261 |
1262 | // **render** is the core function that your view should override, in order
1263 | // to populate its element (`this.el`), with the appropriate HTML. The
1264 | // convention is for **render** to always return `this`.
1265 | render: function() {
1266 | return this;
1267 | },
1268 |
1269 | // Remove this view by taking the element out of the DOM, and removing any
1270 | // applicable Backbone.Events listeners.
1271 | remove: function() {
1272 | this.$el.remove();
1273 | this.stopListening();
1274 | return this;
1275 | },
1276 |
1277 | // Change the view's element (`this.el` property), including event
1278 | // re-delegation.
1279 | setElement: function(element, delegate) {
1280 | if (this.$el) this.undelegateEvents();
1281 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
1282 | this.el = this.$el[0];
1283 | if (delegate !== false) this.delegateEvents();
1284 | return this;
1285 | },
1286 |
1287 | // Set callbacks, where `this.events` is a hash of
1288 | //
1289 | // *{"event selector": "callback"}*
1290 | //
1291 | // {
1292 | // 'mousedown .title': 'edit',
1293 | // 'click .button': 'save'
1294 | // 'click .open': function(e) { ... }
1295 | // }
1296 | //
1297 | // pairs. Callbacks will be bound to the view, with `this` set properly.
1298 | // Uses event delegation for efficiency.
1299 | // Omitting the selector binds the event to `this.el`.
1300 | // This only works for delegate-able events: not `focus`, `blur`, and
1301 | // not `change`, `submit`, and `reset` in Internet Explorer.
1302 | delegateEvents: function(events) {
1303 | if (!(events || (events = _.result(this, 'events')))) return;
1304 | this.undelegateEvents();
1305 | for (var key in events) {
1306 | var method = events[key];
1307 | if (!_.isFunction(method)) method = this[events[key]];
1308 | if (!method) throw new Error('Method "' + events[key] + '" does not exist');
1309 | var match = key.match(delegateEventSplitter);
1310 | var eventName = match[1], selector = match[2];
1311 | method = _.bind(method, this);
1312 | eventName += '.delegateEvents' + this.cid;
1313 | if (selector === '') {
1314 | this.$el.on(eventName, method);
1315 | } else {
1316 | this.$el.on(eventName, selector, method);
1317 | }
1318 | }
1319 | },
1320 |
1321 | // Clears all callbacks previously bound to the view with `delegateEvents`.
1322 | // You usually don't need to use this, but may wish to if you have multiple
1323 | // Backbone views attached to the same DOM element.
1324 | undelegateEvents: function() {
1325 | this.$el.off('.delegateEvents' + this.cid);
1326 | },
1327 |
1328 | // Performs the initial configuration of a View with a set of options.
1329 | // Keys with special meaning *(model, collection, id, className)*, are
1330 | // attached directly to the view.
1331 | _configure: function(options) {
1332 | if (this.options) options = _.extend({}, _.result(this, 'options'), options);
1333 | _.extend(this, _.pick(options, viewOptions));
1334 | this.options = options;
1335 | },
1336 |
1337 | // Ensure that the View has a DOM element to render into.
1338 | // If `this.el` is a string, pass it through `$()`, take the first
1339 | // matching element, and re-assign it to `el`. Otherwise, create
1340 | // an element from the `id`, `className` and `tagName` properties.
1341 | _ensureElement: function() {
1342 | if (!this.el) {
1343 | var attrs = _.extend({}, _.result(this, 'attributes'));
1344 | if (this.id) attrs.id = _.result(this, 'id');
1345 | if (this.className) attrs['class'] = _.result(this, 'className');
1346 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
1347 | this.setElement($el, false);
1348 | } else {
1349 | this.setElement(_.result(this, 'el'), false);
1350 | }
1351 | }
1352 |
1353 | });
1354 |
1355 | // Backbone.sync
1356 | // -------------
1357 |
1358 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1359 | var methodMap = {
1360 | 'create': 'POST',
1361 | 'update': 'PUT',
1362 | 'patch': 'PATCH',
1363 | 'delete': 'DELETE',
1364 | 'read': 'GET'
1365 | };
1366 |
1367 | // Override this function to change the manner in which Backbone persists
1368 | // models to the server. You will be passed the type of request, and the
1369 | // model in question. By default, makes a RESTful Ajax request
1370 | // to the model's `url()`. Some possible customizations could be:
1371 | //
1372 | // * Use `setTimeout` to batch rapid-fire updates into a single request.
1373 | // * Send up the models as XML instead of JSON.
1374 | // * Persist models via WebSockets instead of Ajax.
1375 | //
1376 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1377 | // as `POST`, with a `_method` parameter containing the true HTTP method,
1378 | // as well as all requests with the body as `application/x-www-form-urlencoded`
1379 | // instead of `application/json` with the model in a param named `model`.
1380 | // Useful when interfacing with server-side languages like **PHP** that make
1381 | // it difficult to read the body of `PUT` requests.
1382 | Backbone.sync = function(method, model, options) {
1383 | var type = methodMap[method];
1384 |
1385 | // Default options, unless specified.
1386 | _.defaults(options || (options = {}), {
1387 | emulateHTTP: Backbone.emulateHTTP,
1388 | emulateJSON: Backbone.emulateJSON
1389 | });
1390 |
1391 | // Default JSON-request options.
1392 | var params = {type: type, dataType: 'json'};
1393 |
1394 | // Ensure that we have a URL.
1395 | if (!options.url) {
1396 | params.url = _.result(model, 'url') || urlError();
1397 | }
1398 |
1399 | // Ensure that we have the appropriate request data.
1400 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
1401 | params.contentType = 'application/json';
1402 | params.data = JSON.stringify(options.attrs || model.toJSON(options));
1403 | }
1404 |
1405 | // For older servers, emulate JSON by encoding the request into an HTML-form.
1406 | if (options.emulateJSON) {
1407 | params.contentType = 'application/x-www-form-urlencoded';
1408 | params.data = params.data ? {model: params.data} : {};
1409 | }
1410 |
1411 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1412 | // And an `X-HTTP-Method-Override` header.
1413 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
1414 | params.type = 'POST';
1415 | if (options.emulateJSON) params.data._method = type;
1416 | var beforeSend = options.beforeSend;
1417 | options.beforeSend = function(xhr) {
1418 | xhr.setRequestHeader('X-HTTP-Method-Override', type);
1419 | if (beforeSend) return beforeSend.apply(this, arguments);
1420 | };
1421 | }
1422 |
1423 | // Don't process data on a non-GET request.
1424 | if (params.type !== 'GET' && !options.emulateJSON) {
1425 | params.processData = false;
1426 | }
1427 |
1428 | var success = options.success;
1429 | options.success = function(resp) {
1430 | if (success) success(model, resp, options);
1431 | model.trigger('sync', model, resp, options);
1432 | };
1433 |
1434 | var error = options.error;
1435 | options.error = function(xhr) {
1436 | if (error) error(model, xhr, options);
1437 | model.trigger('error', model, xhr, options);
1438 | };
1439 |
1440 | // Make the request, allowing the user to override any Ajax options.
1441 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
1442 | model.trigger('request', model, xhr, options);
1443 | return xhr;
1444 | };
1445 |
1446 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
1447 | Backbone.ajax = function() {
1448 | return Backbone.$.ajax.apply(Backbone.$, arguments);
1449 | };
1450 |
1451 | // Helpers
1452 | // -------
1453 |
1454 | // Helper function to correctly set up the prototype chain, for subclasses.
1455 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
1456 | // class properties to be extended.
1457 | var extend = function(protoProps, staticProps) {
1458 | var parent = this;
1459 | var child;
1460 |
1461 | // The constructor function for the new subclass is either defined by you
1462 | // (the "constructor" property in your `extend` definition), or defaulted
1463 | // by us to simply call the parent's constructor.
1464 | if (protoProps && _.has(protoProps, 'constructor')) {
1465 | child = protoProps.constructor;
1466 | } else {
1467 | child = function(){ return parent.apply(this, arguments); };
1468 | }
1469 |
1470 | // Add static properties to the constructor function, if supplied.
1471 | _.extend(child, parent, staticProps);
1472 |
1473 | // Set the prototype chain to inherit from `parent`, without calling
1474 | // `parent`'s constructor function.
1475 | var Surrogate = function(){ this.constructor = child; };
1476 | Surrogate.prototype = parent.prototype;
1477 | child.prototype = new Surrogate;
1478 |
1479 | // Add prototype properties (instance properties) to the subclass,
1480 | // if supplied.
1481 | if (protoProps) _.extend(child.prototype, protoProps);
1482 |
1483 | // Set a convenience property in case the parent's prototype is needed
1484 | // later.
1485 | child.__super__ = parent.prototype;
1486 |
1487 | return child;
1488 | };
1489 |
1490 | // Set up inheritance for the model, collection, router, view and history.
1491 | Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
1492 |
1493 | // Throw an error when a URL is needed, and none is supplied.
1494 | var urlError = function() {
1495 | throw new Error('A "url" property or function must be specified');
1496 | };
1497 |
1498 | }).call(this);
1499 |
--------------------------------------------------------------------------------