Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #5 from 3den/all_the_assets

All the assets With Pjax and Rails 4
  • Loading branch information...
commit 204e862accd5d9e773bf60f2dc19d7f1933e0290 2 parents 0e22691 + 3a7dfa6
@steveklabnik authored
View
17 Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-gem 'rails', '3.2.8'
+gem 'rails', '4.0.0'
# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'
@@ -9,17 +9,13 @@ gem 'sqlite3'
gem 'json'
-# Gems used only for assets and not required
-# in production environments by default.
-group :assets do
- gem 'sass-rails', '~> 3.2.3'
- gem 'coffee-rails', '~> 3.2.1'
+gem 'sass-rails', '~> 4.0.0'
+gem 'coffee-rails', '~> 4.0.0'
- # See https://github.com/sstephenson/execjs#readme for more supported runtimes
- # gem 'therubyracer', :platforms => :ruby
+# See https://github.com/sstephenson/execjs#readme for more supported runtimes
+# gem 'therubyracer', :platforms => :ruby
- gem 'uglifier', '>= 1.0.3'
-end
+gem 'uglifier', '>= 1.3.0'
gem 'jquery-rails'
@@ -42,5 +38,6 @@ gem 'turbolinks'
group :test do
gem 'rspec-rails'
+ gem 'selenium-webdriver'
gem 'capybara'
end
View
208 Gemfile.lock
@@ -1,139 +1,132 @@
GEM
remote: https://rubygems.org/
specs:
- actionmailer (3.2.8)
- actionpack (= 3.2.8)
- mail (~> 2.4.4)
- actionpack (3.2.8)
- activemodel (= 3.2.8)
- activesupport (= 3.2.8)
- builder (~> 3.0.0)
+ actionmailer (4.0.0)
+ actionpack (= 4.0.0)
+ mail (~> 2.5.3)
+ actionpack (4.0.0)
+ activesupport (= 4.0.0)
+ builder (~> 3.1.0)
erubis (~> 2.7.0)
- journey (~> 1.0.4)
- rack (~> 1.4.0)
- rack-cache (~> 1.2)
- rack-test (~> 0.6.1)
- sprockets (~> 2.1.3)
- activemodel (3.2.8)
- activesupport (= 3.2.8)
- builder (~> 3.0.0)
- activerecord (3.2.8)
- activemodel (= 3.2.8)
- activesupport (= 3.2.8)
- arel (~> 3.0.2)
- tzinfo (~> 0.3.29)
- activeresource (3.2.8)
- activemodel (= 3.2.8)
- activesupport (= 3.2.8)
- activesupport (3.2.8)
- i18n (~> 0.6)
- multi_json (~> 1.0)
- addressable (2.3.2)
- arel (3.0.2)
- builder (3.0.3)
- capybara (1.1.2)
+ rack (~> 1.5.2)
+ rack-test (~> 0.6.2)
+ activemodel (4.0.0)
+ activesupport (= 4.0.0)
+ builder (~> 3.1.0)
+ activerecord (4.0.0)
+ activemodel (= 4.0.0)
+ activerecord-deprecated_finders (~> 1.0.2)
+ activesupport (= 4.0.0)
+ arel (~> 4.0.0)
+ activerecord-deprecated_finders (1.0.3)
+ activesupport (4.0.0)
+ i18n (~> 0.6, >= 0.6.4)
+ minitest (~> 4.2)
+ multi_json (~> 1.3)
+ thread_safe (~> 0.1)
+ tzinfo (~> 0.3.37)
+ arel (4.0.0)
+ atomic (1.1.14)
+ builder (3.1.4)
+ capybara (2.1.0)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
- selenium-webdriver (~> 2.0)
- xpath (~> 0.1.4)
- childprocess (0.3.5)
- ffi (~> 1.0, >= 1.0.6)
- coffee-rails (3.2.2)
+ xpath (~> 2.0)
+ childprocess (0.3.9)
+ ffi (~> 1.0, >= 1.0.11)
+ coffee-rails (4.0.0)
coffee-script (>= 2.2.0)
- railties (~> 3.2.0)
+ railties (>= 4.0.0.beta, < 5.0)
coffee-script (2.2.0)
coffee-script-source
execjs
- coffee-script-source (1.3.3)
- diff-lcs (1.1.3)
+ coffee-script-source (1.6.3)
+ diff-lcs (1.2.4)
erubis (2.7.0)
- execjs (1.4.0)
- multi_json (~> 1.0)
- ffi (1.1.4)
- hike (1.2.1)
- i18n (0.6.1)
- journey (1.0.4)
- jquery-rails (2.1.3)
- railties (>= 3.1.0, < 5.0)
- thor (~> 0.14)
- json (1.7.5)
- libwebsocket (0.1.5)
- addressable
- mail (2.4.4)
- i18n (>= 0.4.0)
+ execjs (2.0.1)
+ ffi (1.9.0)
+ hike (1.2.3)
+ i18n (0.6.5)
+ jquery-rails (3.0.4)
+ railties (>= 3.0, < 5.0)
+ thor (>= 0.14, < 2.0)
+ json (1.8.0)
+ mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
- mime-types (1.19)
- multi_json (1.3.6)
- nokogiri (1.5.5)
+ mime-types (1.25)
+ mini_portile (0.5.1)
+ minitest (4.7.5)
+ multi_json (1.8.0)
+ nokogiri (1.6.0)
+ mini_portile (~> 0.5.0)
polyglot (0.3.3)
- rack (1.4.1)
- rack-cache (1.2)
- rack (>= 0.4)
- rack-ssl (1.3.2)
- rack
+ rack (1.5.2)
rack-test (0.6.2)
rack (>= 1.0)
- rails (3.2.8)
- actionmailer (= 3.2.8)
- actionpack (= 3.2.8)
- activerecord (= 3.2.8)
- activeresource (= 3.2.8)
- activesupport (= 3.2.8)
- bundler (~> 1.0)
- railties (= 3.2.8)
- railties (3.2.8)
- actionpack (= 3.2.8)
- activesupport (= 3.2.8)
- rack-ssl (~> 1.3.2)
+ rails (4.0.0)
+ actionmailer (= 4.0.0)
+ actionpack (= 4.0.0)
+ activerecord (= 4.0.0)
+ activesupport (= 4.0.0)
+ bundler (>= 1.3.0, < 2.0)
+ railties (= 4.0.0)
+ sprockets-rails (~> 2.0.0)
+ railties (4.0.0)
+ actionpack (= 4.0.0)
+ activesupport (= 4.0.0)
rake (>= 0.8.7)
- rdoc (~> 3.4)
- thor (>= 0.14.6, < 2.0)
- rake (0.9.2.2)
- rdoc (3.12)
- json (~> 1.4)
- rspec (2.11.0)
- rspec-core (~> 2.11.0)
- rspec-expectations (~> 2.11.0)
- rspec-mocks (~> 2.11.0)
- rspec-core (2.11.1)
- rspec-expectations (2.11.3)
- diff-lcs (~> 1.1.3)
- rspec-mocks (2.11.2)
- rspec-rails (2.11.0)
+ thor (>= 0.18.1, < 2.0)
+ rake (10.1.0)
+ rspec-core (2.14.5)
+ rspec-expectations (2.14.2)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rspec-mocks (2.14.3)
+ rspec-rails (2.14.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
- rspec (~> 2.11.0)
+ rspec-core (~> 2.14.0)
+ rspec-expectations (~> 2.14.0)
+ rspec-mocks (~> 2.14.0)
rubyzip (0.9.9)
- sass (3.2.1)
- sass-rails (3.2.5)
- railties (~> 3.2.0)
+ sass (3.2.10)
+ sass-rails (4.0.0)
+ railties (>= 4.0.0.beta, < 5.0)
sass (>= 3.1.10)
- tilt (~> 1.3)
- selenium-webdriver (2.25.0)
+ sprockets-rails (~> 2.0.0)
+ selenium-webdriver (2.35.1)
childprocess (>= 0.2.5)
- libwebsocket (~> 0.1.3)
multi_json (~> 1.0)
- rubyzip
- sprockets (2.1.3)
+ rubyzip (< 1.0.0)
+ websocket (~> 1.0.4)
+ sprockets (2.10.0)
hike (~> 1.2)
+ multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
- sqlite3 (1.3.6)
- thor (0.16.0)
- tilt (1.3.3)
- treetop (1.4.10)
+ sprockets-rails (2.0.0)
+ actionpack (>= 3.0)
+ activesupport (>= 3.0)
+ sprockets (~> 2.8)
+ sqlite3 (1.3.8)
+ thor (0.18.1)
+ thread_safe (0.1.3)
+ atomic
+ tilt (1.4.1)
+ treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
- turbolinks (0.2.1)
- tzinfo (0.3.33)
- uglifier (1.3.0)
+ turbolinks (1.3.0)
+ coffee-rails
+ tzinfo (0.3.37)
+ uglifier (2.2.1)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)
- xpath (0.1.4)
+ websocket (1.0.7)
+ xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS
@@ -141,12 +134,13 @@ PLATFORMS
DEPENDENCIES
capybara
- coffee-rails (~> 3.2.1)
+ coffee-rails (~> 4.0.0)
jquery-rails
json
- rails (= 3.2.8)
+ rails (= 4.0.0)
rspec-rails
- sass-rails (~> 3.2.3)
+ sass-rails (~> 4.0.0)
+ selenium-webdriver
sqlite3
turbolinks
- uglifier (>= 1.0.3)
+ uglifier (>= 1.3.0)
View
60 README.md
@@ -1,40 +1,5 @@
-## Turbolinks test
-
-Well, by now, you've done all your arguing on Twitter. Is Turbolinks a good
-idea, or the Worst Thing Ever?
-
-### optimizing a bit early
-
-![don knuth](http://upload.wikimedia.org/wikipedia/commons/4/4f/KnuthAtOpenContentAlliance.jpg) ![tony hoare](http://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Sir_Tony_Hoare_IMG_5125.jpg/600px-Sir_Tony_Hoare_IMG_5125.jpg)
-
-See these guys? One of them said this:
-
-> "We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil"
-
-Wisdom.
-
-But what makes optimization premature? When you don't know if you should do it
-or not. How do you know?
-
-### measure twice, cut once
-
-![measure twice](http://barnraisersllc.com/wp-content/uploads/2010/08/Measuring-Tapes1.jpg)
-
-Measuring. It's good for you. You can do it. If you measure things, you can be
-sure what's up.
-
-But like eating your veggies, nobody measures. Ever.
-
-### you're a scientist, dammit
-
-Computer **SCIENCE** is called science for a reason, yo. Be a scientist. Don't
-just argue about stuff on blogs. Measure things. Then report back.
-
-### this test sucks
-
-This probably isn't even a good test. I don't care. Tell me how it sucks. Let's
-figure it out. But having actual measurements beats complaining about shit on
-Twitter any day.
+This is a modivication of the original test with Rails 4.
+Also we are testing Pjax vs Turbolinks vs None
### all the assets branch
@@ -51,13 +16,14 @@ Done.
What I get:
-With 1000 pages:
+With 10 pages:
```
$ rspec
user system total real
- no turbolinks 21.990000 2.890000 25.150000 (581.822206)
-yes turbolinks 10.970000 0.910000 11.880000 (196.481247)
+ nothing 1.030000 0.170000 1.530000 ( 5.028314)
+turbolinks 0.140000 0.020000 0.160000 ( 0.984095)
+ pjax 0.100000 0.020000 0.120000 ( 0.876211)
```
With 100 pages:
@@ -65,7 +31,17 @@ With 100 pages:
```
$ rspec
user system total real
- no turbolinks 2.230000 0.300000 2.800000 ( 56.777195)
-yes turbolinks 1.130000 0.090000 1.220000 ( 19.173316)
+ nothing 3.600000 0.800000 4.720000 ( 26.237932)
+turbolinks 1.130000 0.120000 1.250000 ( 11.028930)
+ pjax 1.020000 0.120000 1.140000 ( 7.688372)
```
+With 1000 pages:
+
+```
+$ rspec
+ user system total real
+ nothing 28.410000 7.010000 35.750000 (256.809647)
+turbolinks 10.970000 1.120000 12.090000 (104.501979)
+ pjax 10.240000 1.250000 11.490000 ( 77.392627)
+```
View
21 app/assets/javascripts/application.js
@@ -1,17 +1,10 @@
-// This is a manifest file that'll be compiled into application.js, which will include all the files
-// listed below.
-//
-// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
-// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
-//
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
-// GO AFTER THE REQUIRES BELOW.
-//
//= require jquery
//= require jquery_ujs
-//= require_tree .
-
+//= require jquery.pjax
//= require turbolinks
+
+$(function(){
+ $(document).pjax('a[data-pjax]', 'body');
+});
+
+//= require_tree .
View
15 app/controllers/pages_controller.rb
@@ -1,14 +1,25 @@
class PagesController < ApplicationController
+ layout :set_layout
+
def show
@id = params[:id]
@next_id = @id.to_i + 1
if params[:turbo]
- @link_opts = {}
@url_opts = {:turbo => true}
+ @link_opts = {}
+ elsif params[:pjax]
+ @url_opts = {:pjax => true}
+ @link_opts = {"data-no-turbolink" => true, "data-pjax" => true}
else
@url_opts = {}
- @link_opts = { :"data-no-turbolink" => true}
+ @link_opts = { "data-no-turbolink" => true}
end
end
+
+ private
+
+ def set_layout
+ request.headers['X-PJAX'] ? false : "application"
+ end
end
View
4 app/views/pages/show.html.erb
@@ -1,3 +1,5 @@
<h1>Page <%= @id %></h1>
-<p>Here's a link: <%= link_to "next", page_path(@next_id, @url_opts), @link_opts %></p>
+<p>Here's a link:
+ <%= link_to "next", page_path(@next_id, @url_opts), @link_opts %>
+</p>
View
7 config/application.rb
@@ -2,12 +2,7 @@
require 'rails/all'
-if defined?(Bundler)
- # If you precompile assets before deploying to production, use this line
- Bundler.require(*Rails.groups(:assets => %w(development test)))
- # If you want your assets lazily compiled in production, use this line
- # Bundler.require(:default, :assets, Rails.env)
-end
+Bundler.require(:default, Rails.env)
module TurbolinksTest
class Application < Rails::Application
View
3  config/environments/test.rb
@@ -8,8 +8,7 @@
config.cache_classes = true
# Configure static asset server for tests with Cache-Control for performance
- config.serve_static_assets = true
- config.static_cache_control = "public, max-age=3600"
+ config.assets.compress = false
# Log error messages when you accidentally call methods on nil
config.whiny_nils = true
View
241 public/index.html
@@ -1,241 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <title>Ruby on Rails: Welcome aboard</title>
- <style type="text/css" media="screen">
- body {
- margin: 0;
- margin-bottom: 25px;
- padding: 0;
- background-color: #f0f0f0;
- font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
- font-size: 13px;
- color: #333;
- }
-
- h1 {
- font-size: 28px;
- color: #000;
- }
-
- a {color: #03c}
- a:hover {
- background-color: #03c;
- color: white;
- text-decoration: none;
- }
-
-
- #page {
- background-color: #f0f0f0;
- width: 750px;
- margin: 0;
- margin-left: auto;
- margin-right: auto;
- }
-
- #content {
- float: left;
- background-color: white;
- border: 3px solid #aaa;
- border-top: none;
- padding: 25px;
- width: 500px;
- }
-
- #sidebar {
- float: right;
- width: 175px;
- }
-
- #footer {
- clear: both;
- }
-
- #header, #about, #getting-started {
- padding-left: 75px;
- padding-right: 30px;
- }
-
-
- #header {
- background-image: url("assets/rails.png");
- background-repeat: no-repeat;
- background-position: top left;
- height: 64px;
- }
- #header h1, #header h2 {margin: 0}
- #header h2 {
- color: #888;
- font-weight: normal;
- font-size: 16px;
- }
-
-
- #about h3 {
- margin: 0;
- margin-bottom: 10px;
- font-size: 14px;
- }
-
- #about-content {
- background-color: #ffd;
- border: 1px solid #fc0;
- margin-left: -55px;
- margin-right: -10px;
- }
- #about-content table {
- margin-top: 10px;
- margin-bottom: 10px;
- font-size: 11px;
- border-collapse: collapse;
- }
- #about-content td {
- padding: 10px;
- padding-top: 3px;
- padding-bottom: 3px;
- }
- #about-content td.name {color: #555}
- #about-content td.value {color: #000}
-
- #about-content ul {
- padding: 0;
- list-style-type: none;
- }
-
- #about-content.failure {
- background-color: #fcc;
- border: 1px solid #f00;
- }
- #about-content.failure p {
- margin: 0;
- padding: 10px;
- }
-
-
- #getting-started {
- border-top: 1px solid #ccc;
- margin-top: 25px;
- padding-top: 15px;
- }
- #getting-started h1 {
- margin: 0;
- font-size: 20px;
- }
- #getting-started h2 {
- margin: 0;
- font-size: 14px;
- font-weight: normal;
- color: #333;
- margin-bottom: 25px;
- }
- #getting-started ol {
- margin-left: 0;
- padding-left: 0;
- }
- #getting-started li {
- font-size: 18px;
- color: #888;
- margin-bottom: 25px;
- }
- #getting-started li h2 {
- margin: 0;
- font-weight: normal;
- font-size: 18px;
- color: #333;
- }
- #getting-started li p {
- color: #555;
- font-size: 13px;
- }
-
-
- #sidebar ul {
- margin-left: 0;
- padding-left: 0;
- }
- #sidebar ul h3 {
- margin-top: 25px;
- font-size: 16px;
- padding-bottom: 10px;
- border-bottom: 1px solid #ccc;
- }
- #sidebar li {
- list-style-type: none;
- }
- #sidebar ul.links li {
- margin-bottom: 5px;
- }
-
- .filename {
- font-style: italic;
- }
- </style>
- <script type="text/javascript">
- function about() {
- info = document.getElementById('about-content');
- if (window.XMLHttpRequest)
- { xhr = new XMLHttpRequest(); }
- else
- { xhr = new ActiveXObject("Microsoft.XMLHTTP"); }
- xhr.open("GET","rails/info/properties",false);
- xhr.send("");
- info.innerHTML = xhr.responseText;
- info.style.display = 'block'
- }
- </script>
- </head>
- <body>
- <div id="page">
- <div id="sidebar">
- <ul id="sidebar-items">
- <li>
- <h3>Browse the documentation</h3>
- <ul class="links">
- <li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
- <li><a href="http://api.rubyonrails.org/">Rails API</a></li>
- <li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li>
- <li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li>
- </ul>
- </li>
- </ul>
- </div>
-
- <div id="content">
- <div id="header">
- <h1>Welcome aboard</h1>
- <h2>You&rsquo;re riding Ruby on Rails!</h2>
- </div>
-
- <div id="about">
- <h3><a href="rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
- <div id="about-content" style="display: none"></div>
- </div>
-
- <div id="getting-started">
- <h1>Getting started</h1>
- <h2>Here&rsquo;s how to get rolling:</h2>
-
- <ol>
- <li>
- <h2>Use <code>rails generate</code> to create your models and controllers</h2>
- <p>To see all available options, run it without parameters.</p>
- </li>
-
- <li>
- <h2>Set up a default route and remove <span class="filename">public/index.html</span></h2>
- <p>Routes are set up in <span class="filename">config/routes.rb</span>.</p>
- </li>
-
- <li>
- <h2>Create your database</h2>
- <p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
- </li>
- </ol>
- </div>
- </div>
-
- <div id="footer">&nbsp;</div>
- </div>
- </body>
-</html>
View
12 spec/requests/turbolinks_spec.rb
@@ -6,20 +6,28 @@
n = 1000
Benchmark.bm do |x|
- x.report " no turbolinks" do
+ x.report " nothing" do
visit "/pages/1"
n.times do
click_link "next"
end
end
- x.report "yes turbolinks" do
+ x.report "turbolinks" do
visit "/pages/1?turbo=true"
n.times do
click_link "next"
end
end
+
+ x.report " pjax" do
+ visit "/pages/1?pjax=true"
+
+ n.times do
+ click_link "next"
+ end
+ end
end
end
end
View
26 spec/spec_helper.rb
@@ -1,38 +1,20 @@
# This file is copied to spec/ when you run 'rails generate rspec:install'
-ENV["RAILS_ENV"] ||= 'test'
+ENV["RAILS_ENV"] ||= 'development'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'
+require 'capybara/rails'
+require 'capybara/rspec'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
RSpec.configure do |config|
- # ## Mock Framework
- #
- # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
- #
- # config.mock_with :mocha
- # config.mock_with :flexmock
- # config.mock_with :rr
+ config.include Capybara::DSL
- # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
-
- # If you're not using ActiveRecord, or you'd prefer not to run each of your
- # examples within a transaction, remove the following line or assign false
- # instead of true.
config.use_transactional_fixtures = true
-
- # If true, the base class of anonymous controllers will be inferred
- # automatically. This will be the default behavior in future versions of
- # rspec-rails.
config.infer_base_class_for_anonymous_controllers = false
-
- # Run specs in random order to surface order dependencies. If you find an
- # order dependency and want to debug it, you can fix the order by providing
- # the seed, which is printed after each run.
- # --seed 1234
config.order = "random"
end
View
838 vendor/assets/javascripts/jquery.pjax.js
@@ -0,0 +1,838 @@
+// jquery.pjax.js
+// copyright chris wanstrath
+// https://github.com/defunkt/jquery-pjax
+
+(function($){
+
+// When called on a container with a selector, fetches the href with
+// ajax into the container or with the data-pjax attribute on the link
+// itself.
+//
+// Tries to make sure the back button and ctrl+click work the way
+// you'd expect.
+//
+// Exported as $.fn.pjax
+//
+// Accepts a jQuery ajax options object that may include these
+// pjax specific options:
+//
+//
+// container - Where to stick the response body. Usually a String selector.
+// $(container).html(xhr.responseBody)
+// (default: current jquery context)
+// push - Whether to pushState the URL. Defaults to true (of course).
+// replace - Want to use replaceState instead? That's cool.
+//
+// For convenience the second parameter can be either the container or
+// the options object.
+//
+// Returns the jQuery object
+function fnPjax(selector, container, options) {
+ var context = this
+ return this.on('click.pjax', selector, function(event) {
+ var opts = $.extend({}, optionsFor(container, options))
+ if (!opts.container)
+ opts.container = $(this).attr('data-pjax') || context
+ handleClick(event, opts)
+ })
+}
+
+// Public: pjax on click handler
+//
+// Exported as $.pjax.click.
+//
+// event - "click" jQuery.Event
+// options - pjax options
+//
+// Examples
+//
+// $(document).on('click', 'a', $.pjax.click)
+// // is the same as
+// $(document).pjax('a')
+//
+// $(document).on('click', 'a', function(event) {
+// var container = $(this).closest('[data-pjax-container]')
+// $.pjax.click(event, container)
+// })
+//
+// Returns nothing.
+function handleClick(event, container, options) {
+ options = optionsFor(container, options)
+
+ var link = event.currentTarget
+
+ if (link.tagName.toUpperCase() !== 'A')
+ throw "$.fn.pjax or $.pjax.click requires an anchor element"
+
+ // Middle click, cmd click, and ctrl click should open
+ // links in a new tab as normal.
+ if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )
+ return
+
+ // Ignore cross origin links
+ if ( location.protocol !== link.protocol || location.hostname !== link.hostname )
+ return
+
+ // Ignore anchors on the same page
+ if (link.hash && link.href.replace(link.hash, '') ===
+ location.href.replace(location.hash, ''))
+ return
+
+ // Ignore empty anchor "foo.html#"
+ if (link.href === location.href + '#')
+ return
+
+ var defaults = {
+ url: link.href,
+ container: $(link).attr('data-pjax'),
+ target: link
+ }
+
+ var opts = $.extend({}, defaults, options)
+ var clickEvent = $.Event('pjax:click')
+ $(link).trigger(clickEvent, [opts])
+
+ if (!clickEvent.isDefaultPrevented()) {
+ pjax(opts)
+ event.preventDefault()
+ }
+}
+
+// Public: pjax on form submit handler
+//
+// Exported as $.pjax.submit
+//
+// event - "click" jQuery.Event
+// options - pjax options
+//
+// Examples
+//
+// $(document).on('submit', 'form', function(event) {
+// var container = $(this).closest('[data-pjax-container]')
+// $.pjax.submit(event, container)
+// })
+//
+// Returns nothing.
+function handleSubmit(event, container, options) {
+ options = optionsFor(container, options)
+
+ var form = event.currentTarget
+
+ if (form.tagName.toUpperCase() !== 'FORM')
+ throw "$.pjax.submit requires a form element"
+
+ var defaults = {
+ type: form.method.toUpperCase(),
+ url: form.action,
+ data: $(form).serializeArray(),
+ container: $(form).attr('data-pjax'),
+ target: form
+ }
+
+ pjax($.extend({}, defaults, options))
+
+ event.preventDefault()
+}
+
+// Loads a URL with ajax, puts the response body inside a container,
+// then pushState()'s the loaded URL.
+//
+// Works just like $.ajax in that it accepts a jQuery ajax
+// settings object (with keys like url, type, data, etc).
+//
+// Accepts these extra keys:
+//
+// container - Where to stick the response body.
+// $(container).html(xhr.responseBody)
+// push - Whether to pushState the URL. Defaults to true (of course).
+// replace - Want to use replaceState instead? That's cool.
+//
+// Use it just like $.ajax:
+//
+// var xhr = $.pjax({ url: this.href, container: '#main' })
+// console.log( xhr.readyState )
+//
+// Returns whatever $.ajax returns.
+function pjax(options) {
+ options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)
+
+ if ($.isFunction(options.url)) {
+ options.url = options.url()
+ }
+
+ var target = options.target
+
+ var hash = parseURL(options.url).hash
+
+ var context = options.context = findContainerFor(options.container)
+
+ // We want the browser to maintain two separate internal caches: one
+ // for pjax'd partial page loads and one for normal page loads.
+ // Without adding this secret parameter, some browsers will often
+ // confuse the two.
+ if (!options.data) options.data = {}
+ options.data._pjax = context.selector
+
+ function fire(type, args) {
+ var event = $.Event(type, { relatedTarget: target })
+ context.trigger(event, args)
+ return !event.isDefaultPrevented()
+ }
+
+ var timeoutTimer
+
+ options.beforeSend = function(xhr, settings) {
+ // No timeout for non-GET requests
+ // Its not safe to request the resource again with a fallback method.
+ if (settings.type !== 'GET') {
+ settings.timeout = 0
+ }
+
+ xhr.setRequestHeader('X-PJAX', 'true')
+ xhr.setRequestHeader('X-PJAX-Container', context.selector)
+
+ if (!fire('pjax:beforeSend', [xhr, settings]))
+ return false
+
+ if (settings.timeout > 0) {
+ timeoutTimer = setTimeout(function() {
+ if (fire('pjax:timeout', [xhr, options]))
+ xhr.abort('timeout')
+ }, settings.timeout)
+
+ // Clear timeout setting so jquerys internal timeout isn't invoked
+ settings.timeout = 0
+ }
+
+ options.requestUrl = parseURL(settings.url).href
+ }
+
+ options.complete = function(xhr, textStatus) {
+ if (timeoutTimer)
+ clearTimeout(timeoutTimer)
+
+ fire('pjax:complete', [xhr, textStatus, options])
+
+ fire('pjax:end', [xhr, options])
+ }
+
+ options.error = function(xhr, textStatus, errorThrown) {
+ var container = extractContainer("", xhr, options)
+
+ var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])
+ if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
+ locationReplace(container.url)
+ }
+ }
+
+ options.success = function(data, status, xhr) {
+ // If $.pjax.defaults.version is a function, invoke it first.
+ // Otherwise it can be a static string.
+ var currentVersion = (typeof $.pjax.defaults.version === 'function') ?
+ $.pjax.defaults.version() :
+ $.pjax.defaults.version
+
+ var latestVersion = xhr.getResponseHeader('X-PJAX-Version')
+
+ var container = extractContainer(data, xhr, options)
+
+ // If there is a layout version mismatch, hard load the new url
+ if (currentVersion && latestVersion && currentVersion !== latestVersion) {
+ locationReplace(container.url)
+ return
+ }
+
+ // If the new response is missing a body, hard load the page
+ if (!container.contents) {
+ locationReplace(container.url)
+ return
+ }
+
+ pjax.state = {
+ id: options.id || uniqueId(),
+ url: container.url,
+ title: container.title,
+ container: context.selector,
+ fragment: options.fragment,
+ timeout: options.timeout
+ }
+
+ if (options.push || options.replace) {
+ window.history.replaceState(pjax.state, container.title, container.url)
+ }
+
+ // Clear out any focused controls before inserting new page contents.
+ document.activeElement.blur()
+
+ if (container.title) document.title = container.title
+ context.html(container.contents)
+
+ // FF bug: Won't autofocus fields that are inserted via JS.
+ // This behavior is incorrect. So if theres no current focus, autofocus
+ // the last field.
+ //
+ // http://www.w3.org/html/wg/drafts/html/master/forms.html
+ var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0]
+ if (autofocusEl && document.activeElement !== autofocusEl) {
+ autofocusEl.focus();
+ }
+
+ executeScriptTags(container.scripts)
+
+ // Scroll to top by default
+ if (typeof options.scrollTo === 'number')
+ $(window).scrollTop(options.scrollTo)
+
+ // If the URL has a hash in it, make sure the browser
+ // knows to navigate to the hash.
+ if ( hash !== '' ) {
+ // Avoid using simple hash set here. Will add another history
+ // entry. Replace the url with replaceState and scroll to target
+ // by hand.
+ //
+ // window.location.hash = hash
+ var url = parseURL(container.url)
+ url.hash = hash
+
+ pjax.state.url = url.href
+ window.history.replaceState(pjax.state, container.title, url.href)
+
+ var target = $(url.hash)
+ if (target.length) $(window).scrollTop(target.offset().top)
+ }
+
+ fire('pjax:success', [data, status, xhr, options])
+ }
+
+
+ // Initialize pjax.state for the initial page load. Assume we're
+ // using the container and options of the link we're loading for the
+ // back button to the initial page. This ensures good back button
+ // behavior.
+ if (!pjax.state) {
+ pjax.state = {
+ id: uniqueId(),
+ url: window.location.href,
+ title: document.title,
+ container: context.selector,
+ fragment: options.fragment,
+ timeout: options.timeout
+ }
+ window.history.replaceState(pjax.state, document.title)
+ }
+
+ // Cancel the current request if we're already pjaxing
+ var xhr = pjax.xhr
+ if ( xhr && xhr.readyState < 4) {
+ xhr.onreadystatechange = $.noop
+ xhr.abort()
+ }
+
+ pjax.options = options
+ var xhr = pjax.xhr = $.ajax(options)
+
+ if (xhr.readyState > 0) {
+ if (options.push && !options.replace) {
+ // Cache current container element before replacing it
+ cachePush(pjax.state.id, context.clone().contents())
+
+ window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
+ }
+
+ fire('pjax:start', [xhr, options])
+ fire('pjax:send', [xhr, options])
+ }
+
+ return pjax.xhr
+}
+
+// Public: Reload current page with pjax.
+//
+// Returns whatever $.pjax returns.
+function pjaxReload(container, options) {
+ var defaults = {
+ url: window.location.href,
+ push: false,
+ replace: true,
+ scrollTo: false
+ }
+
+ return pjax($.extend(defaults, optionsFor(container, options)))
+}
+
+// Internal: Hard replace current state with url.
+//
+// Work for around WebKit
+// https://bugs.webkit.org/show_bug.cgi?id=93506
+//
+// Returns nothing.
+function locationReplace(url) {
+ window.history.replaceState(null, "", "#")
+ window.location.replace(url)
+}
+
+
+var initialPop = true
+var initialURL = window.location.href
+var initialState = window.history.state
+
+// Initialize $.pjax.state if possible
+// Happens when reloading a page and coming forward from a different
+// session history.
+if (initialState && initialState.container) {
+ pjax.state = initialState
+}
+
+// Non-webkit browsers don't fire an initial popstate event
+if ('state' in window.history) {
+ initialPop = false
+}
+
+// popstate handler takes care of the back and forward buttons
+//
+// You probably shouldn't use pjax on pages with other pushState
+// stuff yet.
+function onPjaxPopstate(event) {
+ var state = event.state
+
+ if (state && state.container) {
+ // When coming forward from a separate history session, will get an
+ // initial pop with a state we are already at. Skip reloading the current
+ // page.
+ if (initialPop && initialURL == state.url) return
+
+ // If popping back to the same state, just skip.
+ // Could be clicking back from hashchange rather than a pushState.
+ if (pjax.state.id === state.id) return
+
+ var container = $(state.container)
+ if (container.length) {
+ var direction, contents = cacheMapping[state.id]
+
+ if (pjax.state) {
+ // Since state ids always increase, we can deduce the history
+ // direction from the previous state.
+ direction = pjax.state.id < state.id ? 'forward' : 'back'
+
+ // Cache current container before replacement and inform the
+ // cache which direction the history shifted.
+ cachePop(direction, pjax.state.id, container.clone().contents())
+ }
+
+ var popstateEvent = $.Event('pjax:popstate', {
+ state: state,
+ direction: direction
+ })
+ container.trigger(popstateEvent)
+
+ var options = {
+ id: state.id,
+ url: state.url,
+ container: container,
+ push: false,
+ fragment: state.fragment,
+ timeout: state.timeout,
+ scrollTo: false
+ }
+
+ if (contents) {
+ container.trigger('pjax:start', [null, options])
+
+ if (state.title) document.title = state.title
+ container.html(contents)
+ pjax.state = state
+
+ container.trigger('pjax:end', [null, options])
+ } else {
+ pjax(options)
+ }
+
+ // Force reflow/relayout before the browser tries to restore the
+ // scroll position.
+ container[0].offsetHeight
+ } else {
+ locationReplace(location.href)
+ }
+ }
+ initialPop = false
+}
+
+// Fallback version of main pjax function for browsers that don't
+// support pushState.
+//
+// Returns nothing since it retriggers a hard form submission.
+function fallbackPjax(options) {
+ var url = $.isFunction(options.url) ? options.url() : options.url,
+ method = options.type ? options.type.toUpperCase() : 'GET'
+
+ var form = $('<form>', {
+ method: method === 'GET' ? 'GET' : 'POST',
+ action: url,
+ style: 'display:none'
+ })
+
+ if (method !== 'GET' && method !== 'POST') {
+ form.append($('<input>', {
+ type: 'hidden',
+ name: '_method',
+ value: method.toLowerCase()
+ }))
+ }
+
+ var data = options.data
+ if (typeof data === 'string') {
+ $.each(data.split('&'), function(index, value) {
+ var pair = value.split('=')
+ form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
+ })
+ } else if (typeof data === 'object') {
+ for (key in data)
+ form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
+ }
+
+ $(document.body).append(form)
+ form.submit()
+}
+
+// Internal: Generate unique id for state object.
+//
+// Use a timestamp instead of a counter since ids should still be
+// unique across page loads.
+//
+// Returns Number.
+function uniqueId() {
+ return (new Date).getTime()
+}
+
+// Internal: Strips _pjax param from url
+//
+// url - String
+//
+// Returns String.
+function stripPjaxParam(url) {
+ return url
+ .replace(/\?_pjax=[^&]+&?/, '?')
+ .replace(/_pjax=[^&]+&?/, '')
+ .replace(/[\?&]$/, '')
+}
+
+// Internal: Parse URL components and returns a Locationish object.
+//
+// url - String URL
+//
+// Returns HTMLAnchorElement that acts like Location.
+function parseURL(url) {
+ var a = document.createElement('a')
+ a.href = url
+ return a
+}
+
+// Internal: Build options Object for arguments.
+//
+// For convenience the first parameter can be either the container or
+// the options object.
+//
+// Examples
+//
+// optionsFor('#container')
+// // => {container: '#container'}
+//
+// optionsFor('#container', {push: true})
+// // => {container: '#container', push: true}
+//
+// optionsFor({container: '#container', push: true})
+// // => {container: '#container', push: true}
+//
+// Returns options Object.
+function optionsFor(container, options) {
+ // Both container and options
+ if ( container && options )
+ options.container = container
+
+ // First argument is options Object
+ else if ( $.isPlainObject(container) )
+ options = container
+
+ // Only container
+ else
+ options = {container: container}
+
+ // Find and validate container
+ if (options.container)
+ options.container = findContainerFor(options.container)
+
+ return options
+}
+
+// Internal: Find container element for a variety of inputs.
+//
+// Because we can't persist elements using the history API, we must be
+// able to find a String selector that will consistently find the Element.
+//
+// container - A selector String, jQuery object, or DOM Element.
+//
+// Returns a jQuery object whose context is `document` and has a selector.
+function findContainerFor(container) {
+ container = $(container)
+
+ if ( !container.length ) {
+ throw "no pjax container for " + container.selector
+ } else if ( container.selector !== '' && container.context === document ) {
+ return container
+ } else if ( container.attr('id') ) {
+ return $('#' + container.attr('id'))
+ } else {
+ throw "cant get selector for pjax container!"
+ }
+}
+
+// Internal: Filter and find all elements matching the selector.
+//
+// Where $.fn.find only matches descendants, findAll will test all the
+// top level elements in the jQuery object as well.
+//
+// elems - jQuery object of Elements
+// selector - String selector to match
+//
+// Returns a jQuery object.
+function findAll(elems, selector) {
+ return elems.filter(selector).add(elems.find(selector));
+}
+
+function parseHTML(html) {
+ return $.parseHTML(html, document, true)
+}
+
+// Internal: Extracts container and metadata from response.
+//
+// 1. Extracts X-PJAX-URL header if set
+// 2. Extracts inline <title> tags
+// 3. Builds response Element and extracts fragment if set
+//
+// data - String response data
+// xhr - XHR response
+// options - pjax options Object
+//
+// Returns an Object with url, title, and contents keys.
+function extractContainer(data, xhr, options) {
+ var obj = {}
+
+ // Prefer X-PJAX-URL header if it was set, otherwise fallback to
+ // using the original requested url.
+ obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)
+
+ // Attempt to parse response html into elements
+ if (/<html/i.test(data)) {
+ var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))
+ var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
+ } else {
+ var $head = $body = $(parseHTML(data))
+ }
+
+ // If response data is empty, return fast
+ if ($body.length === 0)
+ return obj
+
+ // If there's a <title> tag in the header, use it as
+ // the page's title.
+ obj.title = findAll($head, 'title').last().text()
+
+ if (options.fragment) {
+ // If they specified a fragment, look for it in the response
+ // and pull it out.
+ if (options.fragment === 'body') {
+ var $fragment = $body
+ } else {
+ var $fragment = findAll($body, options.fragment).first()
+ }
+
+ if ($fragment.length) {
+ obj.contents = $fragment.contents()
+
+ // If there's no title, look for data-title and title attributes
+ // on the fragment
+ if (!obj.title)
+ obj.title = $fragment.attr('title') || $fragment.data('title')
+ }
+
+ } else if (!/<html/i.test(data)) {
+ obj.contents = $body
+ }
+
+ // Clean up any <title> tags
+ if (obj.contents) {
+ // Remove any parent title elements
+ obj.contents = obj.contents.not(function() { return $(this).is('title') })
+
+ // Then scrub any titles from their descendants
+ obj.contents.find('title').remove()
+
+ // Gather all script[src] elements
+ obj.scripts = findAll(obj.contents, 'script[src]').remove()
+ obj.contents = obj.contents.not(obj.scripts)
+ }
+
+ // Trim any whitespace off the title
+ if (obj.title) obj.title = $.trim(obj.title)
+
+ return obj
+}
+
+// Load an execute scripts using standard script request.
+//
+// Avoids jQuery's traditional $.getScript which does a XHR request and
+// globalEval.
+//
+// scripts - jQuery object of script Elements
+//
+// Returns nothing.
+function executeScriptTags(scripts) {
+ if (!scripts) return
+
+ var existingScripts = $('script[src]')
+
+ scripts.each(function() {
+ var src = this.src
+ var matchedScripts = existingScripts.filter(function() {
+ return this.src === src
+ })
+ if (matchedScripts.length) return
+
+ var script = document.createElement('script')
+ script.type = $(this).attr('type')
+ script.src = $(this).attr('src')
+ document.head.appendChild(script)
+ })
+}
+
+// Internal: History DOM caching class.
+var cacheMapping = {}
+var cacheForwardStack = []
+var cacheBackStack = []
+
+// Push previous state id and container contents into the history
+// cache. Should be called in conjunction with `pushState` to save the
+// previous container contents.
+//
+// id - State ID Number
+// value - DOM Element to cache
+//
+// Returns nothing.
+function cachePush(id, value) {
+ cacheMapping[id] = value
+ cacheBackStack.push(id)
+
+ // Remove all entires in forward history stack after pushing
+ // a new page.
+ while (cacheForwardStack.length)
+ delete cacheMapping[cacheForwardStack.shift()]
+
+ // Trim back history stack to max cache length.
+ while (cacheBackStack.length > pjax.defaults.maxCacheLength)
+ delete cacheMapping[cacheBackStack.shift()]
+}
+
+// Shifts cache from directional history cache. Should be
+// called on `popstate` with the previous state id and container
+// contents.
+//
+// direction - "forward" or "back" String
+// id - State ID Number
+// value - DOM Element to cache
+//
+// Returns nothing.
+function cachePop(direction, id, value) {
+ var pushStack, popStack
+ cacheMapping[id] = value
+
+ if (direction === 'forward') {
+ pushStack = cacheBackStack
+ popStack = cacheForwardStack
+ } else {
+ pushStack = cacheForwardStack
+ popStack = cacheBackStack
+ }
+
+ pushStack.push(id)
+ if (id = popStack.pop())
+ delete cacheMapping[id]
+}
+
+// Public: Find version identifier for the initial page load.
+//
+// Returns String version or undefined.
+function findVersion() {
+ return $('meta').filter(function() {
+ var name = $(this).attr('http-equiv')
+ return name && name.toUpperCase() === 'X-PJAX-VERSION'
+ }).attr('content')
+}
+
+// Install pjax functions on $.pjax to enable pushState behavior.
+//
+// Does nothing if already enabled.
+//
+// Examples
+//
+// $.pjax.enable()
+//
+// Returns nothing.
+function enable() {
+ $.fn.pjax = fnPjax
+ $.pjax = pjax
+ $.pjax.enable = $.noop
+ $.pjax.disable = disable
+ $.pjax.click = handleClick
+ $.pjax.submit = handleSubmit
+ $.pjax.reload = pjaxReload
+ $.pjax.defaults = {
+ timeout: 650,
+ push: true,
+ replace: false,
+ type: 'GET',
+ dataType: 'html',
+ scrollTo: 0,
+ maxCacheLength: 20,
+ version: findVersion
+ }
+ $(window).on('popstate.pjax', onPjaxPopstate)
+}
+
+// Disable pushState behavior.
+//
+// This is the case when a browser doesn't support pushState. It is
+// sometimes useful to disable pushState for debugging on a modern
+// browser.
+//
+// Examples
+//
+// $.pjax.disable()
+//
+// Returns nothing.
+function disable() {
+ $.fn.pjax = function() { return this }
+ $.pjax = fallbackPjax
+ $.pjax.enable = enable
+ $.pjax.disable = $.noop
+ $.pjax.click = $.noop
+ $.pjax.submit = $.noop
+ $.pjax.reload = function() { window.location.reload() }
+
+ $(window).off('popstate.pjax', onPjaxPopstate)
+}
+
+
+// Add the state property to jQuery's event object so we can use it in
+// $(window).bind('popstate')
+if ( $.inArray('state', $.event.props) < 0 )
+ $.event.props.push('state')
+
+// Is pjax supported by this browser?
+$.support.pjax =
+ window.history && window.history.pushState && window.history.replaceState &&
+ // pushState isn't reliable on iOS until 5.
+ !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
+
+$.support.pjax ? enable() : disable()
+
+})(jQuery);
Please sign in to comment.
Something went wrong with that request. Please try again.