├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── pjax.rb └── pjax_rails.rb ├── pjax_rails.gemspec ├── test ├── controllers │ ├── default_layout_controller_test.rb │ └── with_layout_controller_test.rb ├── dummy │ ├── app │ │ ├── assets │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ └── javascripts │ │ │ │ └── application.js │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ ├── capybara_controller.rb │ │ │ ├── default_layout_controller.rb │ │ │ └── with_layout_controller.rb │ │ └── views │ │ │ ├── capybara │ │ │ ├── html_content.html.erb │ │ │ ├── index.html.erb │ │ │ ├── plain_text.html.erb │ │ │ └── prevents_pjax.html.erb │ │ │ ├── default_layout │ │ │ ├── index.html.erb │ │ │ └── prevent_pjax.html.erb │ │ │ ├── layouts │ │ │ ├── application.html.erb │ │ │ └── with_layout.html.erb │ │ │ └── with_layout │ │ │ └── index.html.erb │ ├── config.ru │ └── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── environment.rb │ │ ├── environments │ │ ├── development.rb │ │ └── test.rb │ │ └── routes.rb ├── features │ └── pjax_test.rb ├── gemfiles │ ├── Gemfile.rails-4.x │ ├── Gemfile.rails-5.x │ ├── Gemfile.rails-6.x │ └── Gemfile.rails-edge └── test_helper.rb └── vendor └── assets └── javascripts └── jquery.pjax.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | Gemfile.lock 3 | test/gemfiles/*.lock 4 | test/dummy/tmp 5 | test/dummy/log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | 4 | before_install: 5 | - gem install -v 1.17.3 bundler 6 | - bundle _1.17.3_ install 7 | 8 | rvm: 9 | - 2.5.7 10 | - 2.6.5 11 | 12 | gemfile: 13 | - test/gemfiles/Gemfile.rails-4.x 14 | - test/gemfiles/Gemfile.rails-5.x 15 | - test/gemfiles/Gemfile.rails-6.x 16 | - test/gemfiles/Gemfile.rails-edge 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 David Heinemeier Hansson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PJAX for Rails 2 | =================== 3 | [![Build Status](https://travis-ci.org/rails/pjax_rails.png?branch=master)](https://travis-ci.org/rails/pjax_rails) 4 | 5 | Integrate Chris Wanstrath's [PJAX](https://github.com/defunkt/jquery-pjax) into 6 | Rails via the asset pipeline. 7 | 8 | To activate, add this to your app/assets/javascripts/application.js (or whatever 9 | bundle you use): 10 | 11 | ```js 12 | //=require jquery.pjax 13 | ``` 14 | 15 | Then choose all the types of links you want to exhibit PJAX: 16 | 17 | ```js 18 | // app/assets/javascripts/application.js 19 | $(function() { 20 | $(document).pjax('a:not([data-remote]):not([data-behavior]):not([data-skip-pjax])', '[data-pjax-container]') 21 | }); 22 | ``` 23 | 24 | For this example, the PJAX container has to be marked with data-pjax-container 25 | attribute, so for example: 26 | 27 | ```erb 28 | 29 |
30 | 31 | <%= Time.now %> 32 |
33 | 34 |
35 | 36 | <%= content_tag :h3, 'My site' %> 37 | <%= link_to 'About me', about_me_path %> 38 | 39 | <%= link_to 'Google', 'http://google.com', 'data-skip-pjax' => true %> 40 |
41 | 42 | ``` 43 | 44 | ## Layouts 45 | 46 | By default, the `pjax_rails` gem will not render your application layout file 47 | and will instead only return the yielded view. But if you have additional 48 | content you want to always be returned with your pjax requests, you can override 49 | `pjax_layout` in your controller and specify a layout to render (by default, 50 | it's `false`) 51 | 52 | ```ruby 53 | class ApplicationController < ActionController::Base 54 | def pjax_layout 55 | 'pjax' 56 | end 57 | end 58 | ``` 59 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'bundler/gem_tasks' 3 | require 'rake/testtask' 4 | 5 | task :default => :test 6 | Rake::TestTask.new do |t| 7 | t.libs << 'test' 8 | t.pattern = ['test/controllers/*_test.rb', 'test/features/*_test.rb'] 9 | t.verbose = true 10 | end 11 | 12 | desc 'Update jquery-pjax to last version' 13 | task :update do 14 | require 'open-uri' 15 | version = open('https://github.com/defunkt/jquery-pjax/commit/master').readlines.grep(//)[0][/\b[0-9a-f\.]+\b/, 0] 16 | message = "Update pjax to #{version}" 17 | 18 | data = open('https://raw.github.com/defunkt/jquery-pjax/master/jquery.pjax.js').read 19 | File.open('vendor/assets/javascripts/jquery.pjax.js', 'w') { |f| f.write data } 20 | 21 | sh 'git add vendor/assets/javascripts/jquery.pjax.js' 22 | sh "git commit -m '#{message}'" 23 | end 24 | -------------------------------------------------------------------------------- /lib/pjax.rb: -------------------------------------------------------------------------------- 1 | module Pjax 2 | class Error < StandardError; end 3 | class Unsupported < Error; end 4 | 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | layout proc { |c| pjax_request? ? pjax_layout : nil } 9 | helper_method :pjax_request? 10 | 11 | rescue_from Pjax::Unsupported, :with => :pjax_unsupported 12 | 13 | if respond_to? :before_action 14 | before_action :strip_pjax_param, :if => :pjax_request? 15 | before_action :set_pjax_url, :if => :pjax_request? 16 | else 17 | before_filter :strip_pjax_param, :if => :pjax_request? 18 | before_filter :set_pjax_url, :if => :pjax_request? 19 | end 20 | end 21 | 22 | protected 23 | 24 | def pjax_request? 25 | request.env['HTTP_X_PJAX'].present? 26 | end 27 | 28 | def pjax_layout 29 | false 30 | end 31 | 32 | def pjax_container 33 | return unless pjax_request? 34 | request.headers['X-PJAX-Container'] 35 | end 36 | 37 | def pjax_unsupported 38 | head :not_acceptable 39 | end 40 | 41 | # Call in a before_action or in an action to disable pjax on an action. 42 | # 43 | # Examples 44 | # 45 | # before_action :prevent_pjax! 46 | # 47 | # def login 48 | # prevent_pjax! 49 | # # ... 50 | # end 51 | # 52 | def prevent_pjax! 53 | raise Pjax::Unsupported if pjax_request? 54 | end 55 | 56 | def strip_pjax_param 57 | params.delete(:_pjax) 58 | request.env['QUERY_STRING'] = request.env['QUERY_STRING'].sub(/\A_pjax=[^&]+&?|&_pjax=[^&]+/, '') 59 | 60 | request.env.delete('rack.request.query_string') 61 | request.env.delete('rack.request.query_hash') 62 | request.env.delete('action_dispatch.request.query_parameters') 63 | 64 | request.instance_variable_set('@original_fullpath', nil) 65 | request.instance_variable_set('@fullpath', nil) 66 | end 67 | 68 | def set_pjax_url 69 | response.headers['X-PJAX-URL'] = request.url 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/pjax_rails.rb: -------------------------------------------------------------------------------- 1 | require 'pjax' 2 | 3 | module PjaxRails 4 | class Engine < ::Rails::Engine 5 | initializer 'pjax_rails.add_controller' do 6 | ActiveSupport.on_load :action_controller do 7 | ActionController::Base.send :include, Pjax 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /pjax_rails.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'pjax_rails' 3 | s.version = '0.5.1' 4 | s.author = 'David Heinemeier Hansson (PJAX by Chris Wanstrath)' 5 | s.email = 'david@loudthinking.com' 6 | s.summary = 'PJAX integration for Rails' 7 | s.homepage = 'https://github.com/rails/pjax_rails' 8 | s.license = 'MIT' 9 | 10 | s.files = Dir['lib/**/*.rb', 'lib/**/*.js', 'vendor/**/*.js'] 11 | 12 | s.add_dependency 'railties', '>= 4.0' 13 | s.add_dependency 'jquery-rails' 14 | 15 | s.add_development_dependency 'rake' 16 | s.add_development_dependency 'rails' 17 | s.add_development_dependency 'capybara' 18 | s.add_development_dependency 'cuprite' 19 | end 20 | -------------------------------------------------------------------------------- /test/controllers/default_layout_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Hash 4 | # override Hash#to_query to prevent sorting of params 5 | def to_query(namespace = nil) 6 | collect do |key, value| 7 | unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? 8 | value.to_query(namespace ? "#{namespace}[#{key}]" : key) 9 | end 10 | end.compact * "&" 11 | end 12 | end 13 | 14 | class DefaultLayoutControllerTest < ActionController::TestCase 15 | test 'renders without layout' do 16 | request.env['HTTP_X_PJAX'] = true 17 | 18 | get :index 19 | 20 | if Rails::VERSION::STRING >= '4.0.0' 21 | assert_match 'default_layout#index', response.body 22 | else 23 | # The behavior for ~> 3.0 varies from 4.0. If there is a layout for parent 24 | # controller and `layout` in parent controller is set to false it will be 25 | # rendered anyway with a warning in a log file. It should be set explicit 26 | # in child controller. 27 | assert_match 'layouts/application default_layout#index', response.body 28 | end 29 | end 30 | 31 | test 'renders with default layout' do 32 | get :index 33 | 34 | assert_match 'layouts/application default_layout#index', response.body 35 | end 36 | 37 | test 'prevents pjax' do 38 | request.env['HTTP_X_PJAX'] = true 39 | 40 | get :prevent_pjax 41 | 42 | assert_equal 406, response.status 43 | end 44 | 45 | test 'strips pjax params' do 46 | request.env['HTTP_X_PJAX'] = true 47 | 48 | get :index, '_pjax' => true 49 | 50 | assert_equal({ 'controller' => 'default_layout', 'action' => 'index' }, Hash[@controller.params]) 51 | assert_nil request.env['rack.request.query_string'] 52 | assert_nil request.env['rack.request.query_hash'] 53 | assert_nil request.env['action_dispatch.request.query_parameters'] 54 | assert_equal '/default_layout', request.original_fullpath 55 | assert_equal '/default_layout', request.fullpath 56 | end 57 | 58 | test 'strips pjax params with multiple params at the beginning' do 59 | request.env['HTTP_X_PJAX'] = true 60 | 61 | get :index, '_pjax' => true, 'first' => '1', 'second' => '2' 62 | 63 | assert_equal({ 'controller' => 'default_layout', 'action' => 'index', 'first' => '1', 'second' => '2' }, Hash[@controller.params]) 64 | assert_nil request.env['rack.request.query_string'] 65 | assert_nil request.env['rack.request.query_hash'] 66 | assert_nil request.env['action_dispatch.request.query_parameters'] 67 | assert_equal '/default_layout?first=1&second=2', request.original_fullpath 68 | assert_equal '/default_layout?first=1&second=2', request.fullpath 69 | end 70 | 71 | test 'strips pjax params with multiple params at the middle' do 72 | request.env['HTTP_X_PJAX'] = true 73 | 74 | get :index, 'first' => '1', '_pjax' => true, 'second' => '2' 75 | 76 | assert_equal({ 'controller' => 'default_layout', 'action' => 'index', 'first' => '1', 'second' => '2' }, Hash[@controller.params]) 77 | assert_nil request.env['rack.request.query_string'] 78 | assert_nil request.env['rack.request.query_hash'] 79 | assert_nil request.env['action_dispatch.request.query_parameters'] 80 | assert_equal '/default_layout?first=1&second=2', request.original_fullpath 81 | assert_equal '/default_layout?first=1&second=2', request.fullpath 82 | end 83 | 84 | test 'strips pjax params with multiple params at the end' do 85 | request.env['HTTP_X_PJAX'] = true 86 | 87 | get :index, 'first' => '1', 'second' => '2', '_pjax' => true 88 | 89 | assert_equal({ 'controller' => 'default_layout', 'action' => 'index', 'first' => '1', 'second' => '2' }, Hash[@controller.params]) 90 | assert_nil request.env['rack.request.query_string'] 91 | assert_nil request.env['rack.request.query_hash'] 92 | assert_nil request.env['action_dispatch.request.query_parameters'] 93 | assert_equal '/default_layout?first=1&second=2', request.original_fullpath 94 | assert_equal '/default_layout?first=1&second=2', request.fullpath 95 | end 96 | 97 | test 'sets pjax url' do 98 | request.env['HTTP_X_PJAX'] = true 99 | 100 | get :index 101 | 102 | assert_equal 'http://test.host/default_layout', response.headers['X-PJAX-URL'] 103 | end 104 | 105 | def get(action, params = {}) 106 | if Rails::VERSION::STRING >= '5.0.0' 107 | super(action, { params: params }) 108 | else 109 | super(action, params) 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /test/controllers/with_layout_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class WithLayoutControllerTest < ActionController::TestCase 4 | test 'renders with layout for pjax request' do 5 | request.env['HTTP_X_PJAX'] = true 6 | 7 | get :index 8 | 9 | assert_match 'layouts/with_layout with_layout#index', response.body 10 | end 11 | 12 | test 'renders with layout for regular request' do 13 | get :index 14 | 15 | assert_match 'layouts/with_layout with_layout#index', response.body 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link application.js 2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | //= require jquery.pjax 4 | //= require_tree . 5 | 6 | $(function() { 7 | $(document).pjax('a:not([data-skip-pjax])', '[data-pjax-container]'); 8 | }); 9 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/capybara_controller.rb: -------------------------------------------------------------------------------- 1 | class CapybaraController < ApplicationController 2 | def prevents_pjax 3 | prevent_pjax! 4 | end 5 | 6 | def favicon 7 | head :not_found 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/default_layout_controller.rb: -------------------------------------------------------------------------------- 1 | class DefaultLayoutController < ApplicationController 2 | def prevent_pjax 3 | prevent_pjax! 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/with_layout_controller.rb: -------------------------------------------------------------------------------- 1 | class WithLayoutController < ApplicationController 2 | layout 'with_layout' 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/views/capybara/html_content.html.erb: -------------------------------------------------------------------------------- 1 | <table> 2 | <tbody> 3 | <tr> 4 | <td>Html content</td> 5 | </tr> 6 | </tbody> 7 | </table> 8 | -------------------------------------------------------------------------------- /test/dummy/app/views/capybara/index.html.erb: -------------------------------------------------------------------------------- 1 | <div>Will not be touched</div> 2 | 3 | <div data-pjax-container> 4 | <%= content_tag :h3, 'Pjax container' %> 5 | <%= link_to 'plainText', '/capybara/plain_text' %> 6 | <%= link_to 'htmlContent', '/capybara/html_content' %> 7 | <%= link_to 'fullReload', '/capybara/plain_text', 'data-skip-pjax' => true %> 8 | <%= link_to 'preventsPjax', '/capybara/prevents_pjax' %> 9 | </div> 10 | -------------------------------------------------------------------------------- /test/dummy/app/views/capybara/plain_text.html.erb: -------------------------------------------------------------------------------- 1 | Plain text -------------------------------------------------------------------------------- /test/dummy/app/views/capybara/prevents_pjax.html.erb: -------------------------------------------------------------------------------- 1 | Prevents pjax -------------------------------------------------------------------------------- /test/dummy/app/views/default_layout/index.html.erb: -------------------------------------------------------------------------------- 1 | default_layout#index -------------------------------------------------------------------------------- /test/dummy/app/views/default_layout/prevent_pjax.html.erb: -------------------------------------------------------------------------------- 1 | default_layout#prevent_pjax -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>Dummy 5 | <%= javascript_include_tag 'application' %> 6 | 7 | 8 | 9 | layouts/application <%= yield %> 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/with_layout.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= javascript_include_tag 'application' %> 6 | 7 | 8 | 9 | layouts/with_layout <%= yield %> 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/dummy/app/views/with_layout/index.html.erb: -------------------------------------------------------------------------------- 1 | with_layout#index -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | require ::File.expand_path('../config/environment', __FILE__) 2 | 3 | run Rails.application 4 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails' 4 | require 'action_controller/railtie' 5 | require 'sprockets/railtie' 6 | 7 | require 'jquery-rails' 8 | require 'pjax_rails' 9 | 10 | Bundler.require(:default, Rails.env) 11 | 12 | module Dummy 13 | class Application < Rails::Application 14 | config.secret_token = 'a966a729a228e5d3edf00997e7b7eab7' 15 | config.assets.enabled = true 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 4 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | config.cache_classes = false 3 | 4 | config.eager_load = false 5 | 6 | config.consider_all_requests_local = true 7 | config.action_controller.perform_caching = false 8 | 9 | config.active_support.deprecation = :log 10 | 11 | config.assets.debug = true 12 | end 13 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | config.cache_classes = true 3 | 4 | config.eager_load = false 5 | 6 | config.serve_static_files = true 7 | 8 | if Rails::VERSION::STRING >= '5.0.0' 9 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } 10 | config.action_controller.permit_all_parameters = true 11 | else 12 | config.static_cache_control = "public, max-age=3600" 13 | end 14 | 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | 18 | config.action_dispatch.show_exceptions = false 19 | 20 | config.action_controller.allow_forgery_protection = false 21 | 22 | config.active_support.deprecation = :notify 23 | end 24 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | get '/:controller(/:action(/:id))' 3 | get '/favicon.ico' => 'capybara#favicon' 4 | end 5 | -------------------------------------------------------------------------------- /test/features/pjax_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PjaxTest < ActiveSupport::IntegrationCase 4 | test 'loads plain text' do 5 | visit '/capybara' 6 | 7 | click_on 'plainText' 8 | 9 | assert page.has_content?('Will not be touched') 10 | assert page.has_no_content?('Pjax container') 11 | assert page.has_content?('Plain text') 12 | end 13 | 14 | test 'loads html' do 15 | visit '/capybara' 16 | 17 | click_on 'htmlContent' 18 | 19 | assert page.has_content?('Will not be touched') 20 | assert page.has_no_content?('Pjax container') 21 | assert_equal page.find(:xpath, '//table/tbody/tr/td').text, 'Html content' 22 | end 23 | 24 | test 'fully reloads page' do 25 | visit '/capybara' 26 | 27 | click_on 'fullReload' 28 | 29 | assert page.has_no_content?('Will not be touched') 30 | assert page.has_content?('layouts/application Plain text') 31 | end 32 | 33 | test 'prevents pjax' do 34 | visit '/capybara' 35 | 36 | click_on 'preventsPjax' 37 | 38 | assert page.has_no_content?('Will not be touched') 39 | assert page.has_content?('layouts/application Prevents pjax') 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.rails-4.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec :path => './../..' 3 | 4 | gem 'rails', '~> 4' 5 | gem 'mime-types', '< 3' 6 | gem 'addressable' 7 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.rails-5.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec :path => './../..' 3 | 4 | gem 'rails', '~> 5.0' 5 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.rails-6.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec :path => './../..' 3 | 4 | gem 'rails', '~> 6.0' 5 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.rails-edge: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec :path => './../..' 3 | 4 | gem 'rails', git: 'https://github.com/rails/rails.git' 5 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | 3 | require 'dummy/config/environment' 4 | require 'rails/test_help' 5 | require 'capybara/rails' 6 | require 'capybara/cuprite' 7 | 8 | Capybara.app = Rails.application 9 | Capybara.server = :webrick 10 | Capybara.default_driver = :cuprite 11 | 12 | class ActiveSupport::IntegrationCase < ActiveSupport::TestCase 13 | include Capybara::DSL 14 | 15 | def teardown 16 | Capybara.reset_sessions! 17 | Capybara.use_default_driver 18 | end 19 | end 20 | 21 | # Rails 4.2 call `initialize` inside `recycle!`. 22 | # However Ruby 2.6 doesn't allow calling `initialize` twice. 23 | # More info: https://github.com/rails/rails/issues/34790 24 | if RUBY_VERSION >= "2.6.0" && Rails.version < "5" 25 | module ActionController 26 | class TestResponse < ActionDispatch::TestResponse 27 | def recycle! 28 | @mon_mutex_owner_object_id = nil 29 | @mon_mutex = nil 30 | initialize 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/jquery.pjax.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2012, Chris Wanstrath 3 | * Released under the MIT License 4 | * https://github.com/defunkt/jquery-pjax 5 | */ 6 | 7 | (function($){ 8 | 9 | // When called on a container with a selector, fetches the href with 10 | // ajax into the container or with the data-pjax attribute on the link 11 | // itself. 12 | // 13 | // Tries to make sure the back button and ctrl+click work the way 14 | // you'd expect. 15 | // 16 | // Exported as $.fn.pjax 17 | // 18 | // Accepts a jQuery ajax options object that may include these 19 | // pjax specific options: 20 | // 21 | // 22 | // container - String selector for the element where to place the response body. 23 | // push - Whether to pushState the URL. Defaults to true (of course). 24 | // replace - Want to use replaceState instead? That's cool. 25 | // 26 | // For convenience the second parameter can be either the container or 27 | // the options object. 28 | // 29 | // Returns the jQuery object 30 | function fnPjax(selector, container, options) { 31 | options = optionsFor(container, options) 32 | return this.on('click.pjax', selector, function(event) { 33 | var opts = options 34 | if (!opts.container) { 35 | opts = $.extend({}, options) 36 | opts.container = $(this).attr('data-pjax') 37 | } 38 | handleClick(event, opts) 39 | }) 40 | } 41 | 42 | // Public: pjax on click handler 43 | // 44 | // Exported as $.pjax.click. 45 | // 46 | // event - "click" jQuery.Event 47 | // options - pjax options 48 | // 49 | // Examples 50 | // 51 | // $(document).on('click', 'a', $.pjax.click) 52 | // // is the same as 53 | // $(document).pjax('a') 54 | // 55 | // Returns nothing. 56 | function handleClick(event, container, options) { 57 | options = optionsFor(container, options) 58 | 59 | var link = event.currentTarget 60 | var $link = $(link) 61 | 62 | if (link.tagName.toUpperCase() !== 'A') 63 | throw "$.fn.pjax or $.pjax.click requires an anchor element" 64 | 65 | // Middle click, cmd click, and ctrl click should open 66 | // links in a new tab as normal. 67 | if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) 68 | return 69 | 70 | // Ignore cross origin links 71 | if ( location.protocol !== link.protocol || location.hostname !== link.hostname ) 72 | return 73 | 74 | // Ignore case when a hash is being tacked on the current URL 75 | if ( link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location) ) 76 | return 77 | 78 | // Ignore event with default prevented 79 | if (event.isDefaultPrevented()) 80 | return 81 | 82 | var defaults = { 83 | url: link.href, 84 | container: $link.attr('data-pjax'), 85 | target: link 86 | } 87 | 88 | var opts = $.extend({}, defaults, options) 89 | var clickEvent = $.Event('pjax:click') 90 | $link.trigger(clickEvent, [opts]) 91 | 92 | if (!clickEvent.isDefaultPrevented()) { 93 | pjax(opts) 94 | event.preventDefault() 95 | $link.trigger('pjax:clicked', [opts]) 96 | } 97 | } 98 | 99 | // Public: pjax on form submit handler 100 | // 101 | // Exported as $.pjax.submit 102 | // 103 | // event - "click" jQuery.Event 104 | // options - pjax options 105 | // 106 | // Examples 107 | // 108 | // $(document).on('submit', 'form', function(event) { 109 | // $.pjax.submit(event, '[data-pjax-container]') 110 | // }) 111 | // 112 | // Returns nothing. 113 | function handleSubmit(event, container, options) { 114 | options = optionsFor(container, options) 115 | 116 | var form = event.currentTarget 117 | var $form = $(form) 118 | 119 | if (form.tagName.toUpperCase() !== 'FORM') 120 | throw "$.pjax.submit requires a form element" 121 | 122 | var defaults = { 123 | type: ($form.attr('method') || 'GET').toUpperCase(), 124 | url: $form.attr('action'), 125 | container: $form.attr('data-pjax'), 126 | target: form 127 | } 128 | 129 | if (defaults.type !== 'GET' && window.FormData !== undefined) { 130 | defaults.data = new FormData(form) 131 | defaults.processData = false 132 | defaults.contentType = false 133 | } else { 134 | // Can't handle file uploads, exit 135 | if ($form.find(':file').length) { 136 | return 137 | } 138 | 139 | // Fallback to manually serializing the fields 140 | defaults.data = $form.serializeArray() 141 | } 142 | 143 | pjax($.extend({}, defaults, options)) 144 | 145 | event.preventDefault() 146 | } 147 | 148 | // Loads a URL with ajax, puts the response body inside a container, 149 | // then pushState()'s the loaded URL. 150 | // 151 | // Works just like $.ajax in that it accepts a jQuery ajax 152 | // settings object (with keys like url, type, data, etc). 153 | // 154 | // Accepts these extra keys: 155 | // 156 | // container - String selector for where to stick the response body. 157 | // push - Whether to pushState the URL. Defaults to true (of course). 158 | // replace - Want to use replaceState instead? That's cool. 159 | // 160 | // Use it just like $.ajax: 161 | // 162 | // var xhr = $.pjax({ url: this.href, container: '#main' }) 163 | // console.log( xhr.readyState ) 164 | // 165 | // Returns whatever $.ajax returns. 166 | function pjax(options) { 167 | options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options) 168 | 169 | if ($.isFunction(options.url)) { 170 | options.url = options.url() 171 | } 172 | 173 | var hash = parseURL(options.url).hash 174 | 175 | var containerType = $.type(options.container) 176 | if (containerType !== 'string') { 177 | throw "expected string value for 'container' option; got " + containerType 178 | } 179 | var context = options.context = $(options.container) 180 | if (!context.length) { 181 | throw "the container selector '" + options.container + "' did not match anything" 182 | } 183 | 184 | // We want the browser to maintain two separate internal caches: one 185 | // for pjax'd partial page loads and one for normal page loads. 186 | // Without adding this secret parameter, some browsers will often 187 | // confuse the two. 188 | if (!options.data) options.data = {} 189 | if ($.isArray(options.data)) { 190 | options.data.push({name: '_pjax', value: options.container}) 191 | } else { 192 | options.data._pjax = options.container 193 | } 194 | 195 | function fire(type, args, props) { 196 | if (!props) props = {} 197 | props.relatedTarget = options.target 198 | var event = $.Event(type, props) 199 | context.trigger(event, args) 200 | return !event.isDefaultPrevented() 201 | } 202 | 203 | var timeoutTimer 204 | 205 | options.beforeSend = function(xhr, settings) { 206 | // No timeout for non-GET requests 207 | // Its not safe to request the resource again with a fallback method. 208 | if (settings.type !== 'GET') { 209 | settings.timeout = 0 210 | } 211 | 212 | xhr.setRequestHeader('X-PJAX', 'true') 213 | xhr.setRequestHeader('X-PJAX-Container', options.container) 214 | 215 | if (!fire('pjax:beforeSend', [xhr, settings])) 216 | return false 217 | 218 | if (settings.timeout > 0) { 219 | timeoutTimer = setTimeout(function() { 220 | if (fire('pjax:timeout', [xhr, options])) 221 | xhr.abort('timeout') 222 | }, settings.timeout) 223 | 224 | // Clear timeout setting so jQuerys internal timeout isn't invoked 225 | settings.timeout = 0 226 | } 227 | 228 | var url = parseURL(settings.url) 229 | if (hash) url.hash = hash 230 | options.requestUrl = stripInternalParams(url) 231 | } 232 | 233 | options.complete = function(xhr, textStatus) { 234 | if (timeoutTimer) 235 | clearTimeout(timeoutTimer) 236 | 237 | fire('pjax:complete', [xhr, textStatus, options]) 238 | 239 | fire('pjax:end', [xhr, options]) 240 | } 241 | 242 | options.error = function(xhr, textStatus, errorThrown) { 243 | var container = extractContainer("", xhr, options) 244 | 245 | var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]) 246 | if (options.type == 'GET' && textStatus !== 'abort' && allowed) { 247 | locationReplace(container.url) 248 | } 249 | } 250 | 251 | options.success = function(data, status, xhr) { 252 | var previousState = pjax.state 253 | 254 | // If $.pjax.defaults.version is a function, invoke it first. 255 | // Otherwise it can be a static string. 256 | var currentVersion = typeof $.pjax.defaults.version === 'function' ? 257 | $.pjax.defaults.version() : 258 | $.pjax.defaults.version 259 | 260 | var latestVersion = xhr.getResponseHeader('X-PJAX-Version') 261 | 262 | var container = extractContainer(data, xhr, options) 263 | 264 | var url = parseURL(container.url) 265 | if (hash) { 266 | url.hash = hash 267 | container.url = url.href 268 | } 269 | 270 | // If there is a layout version mismatch, hard load the new url 271 | if (currentVersion && latestVersion && currentVersion !== latestVersion) { 272 | locationReplace(container.url) 273 | return 274 | } 275 | 276 | // If the new response is missing a body, hard load the page 277 | if (!container.contents) { 278 | locationReplace(container.url) 279 | return 280 | } 281 | 282 | pjax.state = { 283 | id: options.id || uniqueId(), 284 | url: container.url, 285 | title: container.title, 286 | container: options.container, 287 | fragment: options.fragment, 288 | timeout: options.timeout 289 | } 290 | 291 | if (options.push || options.replace) { 292 | window.history.replaceState(pjax.state, container.title, container.url) 293 | } 294 | 295 | // Only blur the focus if the focused element is within the container. 296 | var blurFocus = $.contains(context, document.activeElement) 297 | 298 | // Clear out any focused controls before inserting new page contents. 299 | if (blurFocus) { 300 | try { 301 | document.activeElement.blur() 302 | } catch (e) { /* ignore */ } 303 | } 304 | 305 | if (container.title) document.title = container.title 306 | 307 | fire('pjax:beforeReplace', [container.contents, options], { 308 | state: pjax.state, 309 | previousState: previousState 310 | }) 311 | context.html(container.contents) 312 | 313 | // FF bug: Won't autofocus fields that are inserted via JS. 314 | // This behavior is incorrect. So if theres no current focus, autofocus 315 | // the last field. 316 | // 317 | // http://www.w3.org/html/wg/drafts/html/master/forms.html 318 | var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0] 319 | if (autofocusEl && document.activeElement !== autofocusEl) { 320 | autofocusEl.focus() 321 | } 322 | 323 | executeScriptTags(container.scripts) 324 | 325 | var scrollTo = options.scrollTo 326 | 327 | // Ensure browser scrolls to the element referenced by the URL anchor 328 | if (hash) { 329 | var name = decodeURIComponent(hash.slice(1)) 330 | var target = document.getElementById(name) || document.getElementsByName(name)[0] 331 | if (target) scrollTo = $(target).offset().top 332 | } 333 | 334 | if (typeof scrollTo == 'number') $(window).scrollTop(scrollTo) 335 | 336 | fire('pjax:success', [data, status, xhr, options]) 337 | } 338 | 339 | 340 | // Initialize pjax.state for the initial page load. Assume we're 341 | // using the container and options of the link we're loading for the 342 | // back button to the initial page. This ensures good back button 343 | // behavior. 344 | if (!pjax.state) { 345 | pjax.state = { 346 | id: uniqueId(), 347 | url: window.location.href, 348 | title: document.title, 349 | container: options.container, 350 | fragment: options.fragment, 351 | timeout: options.timeout 352 | } 353 | window.history.replaceState(pjax.state, document.title) 354 | } 355 | 356 | // Cancel the current request if we're already pjaxing 357 | abortXHR(pjax.xhr) 358 | 359 | pjax.options = options 360 | var xhr = pjax.xhr = $.ajax(options) 361 | 362 | if (xhr.readyState > 0) { 363 | if (options.push && !options.replace) { 364 | // Cache current container element before replacing it 365 | cachePush(pjax.state.id, [options.container, cloneContents(context)]) 366 | 367 | window.history.pushState(null, "", options.requestUrl) 368 | } 369 | 370 | fire('pjax:start', [xhr, options]) 371 | fire('pjax:send', [xhr, options]) 372 | } 373 | 374 | return pjax.xhr 375 | } 376 | 377 | // Public: Reload current page with pjax. 378 | // 379 | // Returns whatever $.pjax returns. 380 | function pjaxReload(container, options) { 381 | var defaults = { 382 | url: window.location.href, 383 | push: false, 384 | replace: true, 385 | scrollTo: false 386 | } 387 | 388 | return pjax($.extend(defaults, optionsFor(container, options))) 389 | } 390 | 391 | // Internal: Hard replace current state with url. 392 | // 393 | // Work for around WebKit 394 | // https://bugs.webkit.org/show_bug.cgi?id=93506 395 | // 396 | // Returns nothing. 397 | function locationReplace(url) { 398 | window.history.replaceState(null, "", pjax.state.url) 399 | window.location.replace(url) 400 | } 401 | 402 | 403 | var initialPop = true 404 | var initialURL = window.location.href 405 | var initialState = window.history.state 406 | 407 | // Initialize $.pjax.state if possible 408 | // Happens when reloading a page and coming forward from a different 409 | // session history. 410 | if (initialState && initialState.container) { 411 | pjax.state = initialState 412 | } 413 | 414 | // Non-webkit browsers don't fire an initial popstate event 415 | if ('state' in window.history) { 416 | initialPop = false 417 | } 418 | 419 | // popstate handler takes care of the back and forward buttons 420 | // 421 | // You probably shouldn't use pjax on pages with other pushState 422 | // stuff yet. 423 | function onPjaxPopstate(event) { 424 | 425 | // Hitting back or forward should override any pending PJAX request. 426 | if (!initialPop) { 427 | abortXHR(pjax.xhr) 428 | } 429 | 430 | var previousState = pjax.state 431 | var state = event.state 432 | var direction 433 | 434 | if (state && state.container) { 435 | // When coming forward from a separate history session, will get an 436 | // initial pop with a state we are already at. Skip reloading the current 437 | // page. 438 | if (initialPop && initialURL == state.url) return 439 | 440 | if (previousState) { 441 | // If popping back to the same state, just skip. 442 | // Could be clicking back from hashchange rather than a pushState. 443 | if (previousState.id === state.id) return 444 | 445 | // Since state IDs always increase, we can deduce the navigation direction 446 | direction = previousState.id < state.id ? 'forward' : 'back' 447 | } 448 | 449 | var cache = cacheMapping[state.id] || [] 450 | var containerSelector = cache[0] || state.container 451 | var container = $(containerSelector), contents = cache[1] 452 | 453 | if (container.length) { 454 | if (previousState) { 455 | // Cache current container before replacement and inform the 456 | // cache which direction the history shifted. 457 | cachePop(direction, previousState.id, [containerSelector, cloneContents(container)]) 458 | } 459 | 460 | var popstateEvent = $.Event('pjax:popstate', { 461 | state: state, 462 | direction: direction 463 | }) 464 | container.trigger(popstateEvent) 465 | 466 | var options = { 467 | id: state.id, 468 | url: state.url, 469 | container: containerSelector, 470 | push: false, 471 | fragment: state.fragment, 472 | timeout: state.timeout, 473 | scrollTo: false 474 | } 475 | 476 | if (contents) { 477 | container.trigger('pjax:start', [null, options]) 478 | 479 | pjax.state = state 480 | if (state.title) document.title = state.title 481 | var beforeReplaceEvent = $.Event('pjax:beforeReplace', { 482 | state: state, 483 | previousState: previousState 484 | }) 485 | container.trigger(beforeReplaceEvent, [contents, options]) 486 | container.html(contents) 487 | 488 | container.trigger('pjax:end', [null, options]) 489 | } else { 490 | pjax(options) 491 | } 492 | 493 | // Force reflow/relayout before the browser tries to restore the 494 | // scroll position. 495 | container[0].offsetHeight // eslint-disable-line no-unused-expressions 496 | } else { 497 | locationReplace(location.href) 498 | } 499 | } 500 | initialPop = false 501 | } 502 | 503 | // Fallback version of main pjax function for browsers that don't 504 | // support pushState. 505 | // 506 | // Returns nothing since it retriggers a hard form submission. 507 | function fallbackPjax(options) { 508 | var url = $.isFunction(options.url) ? options.url() : options.url, 509 | method = options.type ? options.type.toUpperCase() : 'GET' 510 | 511 | var form = $('
', { 512 | method: method === 'GET' ? 'GET' : 'POST', 513 | action: url, 514 | style: 'display:none' 515 | }) 516 | 517 | if (method !== 'GET' && method !== 'POST') { 518 | form.append($('', { 519 | type: 'hidden', 520 | name: '_method', 521 | value: method.toLowerCase() 522 | })) 523 | } 524 | 525 | var data = options.data 526 | if (typeof data === 'string') { 527 | $.each(data.split('&'), function(index, value) { 528 | var pair = value.split('=') 529 | form.append($('', {type: 'hidden', name: pair[0], value: pair[1]})) 530 | }) 531 | } else if ($.isArray(data)) { 532 | $.each(data, function(index, value) { 533 | form.append($('', {type: 'hidden', name: value.name, value: value.value})) 534 | }) 535 | } else if (typeof data === 'object') { 536 | var key 537 | for (key in data) 538 | form.append($('', {type: 'hidden', name: key, value: data[key]})) 539 | } 540 | 541 | $(document.body).append(form) 542 | form.submit() 543 | } 544 | 545 | // Internal: Abort an XmlHttpRequest if it hasn't been completed, 546 | // also removing its event handlers. 547 | function abortXHR(xhr) { 548 | if ( xhr && xhr.readyState < 4) { 549 | xhr.onreadystatechange = $.noop 550 | xhr.abort() 551 | } 552 | } 553 | 554 | // Internal: Generate unique id for state object. 555 | // 556 | // Use a timestamp instead of a counter since ids should still be 557 | // unique across page loads. 558 | // 559 | // Returns Number. 560 | function uniqueId() { 561 | return (new Date).getTime() 562 | } 563 | 564 | function cloneContents(container) { 565 | var cloned = container.clone() 566 | // Unmark script tags as already being eval'd so they can get executed again 567 | // when restored from cache. HAXX: Uses jQuery internal method. 568 | cloned.find('script').each(function(){ 569 | if (!this.src) $._data(this, 'globalEval', false) 570 | }) 571 | return cloned.contents() 572 | } 573 | 574 | // Internal: Strip internal query params from parsed URL. 575 | // 576 | // Returns sanitized url.href String. 577 | function stripInternalParams(url) { 578 | url.search = url.search.replace(/([?&])(_pjax|_)=[^&]*/g, '').replace(/^&/, '') 579 | return url.href.replace(/\?($|#)/, '$1') 580 | } 581 | 582 | // Internal: Parse URL components and returns a Locationish object. 583 | // 584 | // url - String URL 585 | // 586 | // Returns HTMLAnchorElement that acts like Location. 587 | function parseURL(url) { 588 | var a = document.createElement('a') 589 | a.href = url 590 | return a 591 | } 592 | 593 | // Internal: Return the `href` component of given URL object with the hash 594 | // portion removed. 595 | // 596 | // location - Location or HTMLAnchorElement 597 | // 598 | // Returns String 599 | function stripHash(location) { 600 | return location.href.replace(/#.*/, '') 601 | } 602 | 603 | // Internal: Build options Object for arguments. 604 | // 605 | // For convenience the first parameter can be either the container or 606 | // the options object. 607 | // 608 | // Examples 609 | // 610 | // optionsFor('#container') 611 | // // => {container: '#container'} 612 | // 613 | // optionsFor('#container', {push: true}) 614 | // // => {container: '#container', push: true} 615 | // 616 | // optionsFor({container: '#container', push: true}) 617 | // // => {container: '#container', push: true} 618 | // 619 | // Returns options Object. 620 | function optionsFor(container, options) { 621 | if (container && options) { 622 | options = $.extend({}, options) 623 | options.container = container 624 | return options 625 | } else if ($.isPlainObject(container)) { 626 | return container 627 | } else { 628 | return {container: container} 629 | } 630 | } 631 | 632 | // Internal: Filter and find all elements matching the selector. 633 | // 634 | // Where $.fn.find only matches descendants, findAll will test all the 635 | // top level elements in the jQuery object as well. 636 | // 637 | // elems - jQuery object of Elements 638 | // selector - String selector to match 639 | // 640 | // Returns a jQuery object. 641 | function findAll(elems, selector) { 642 | return elems.filter(selector).add(elems.find(selector)) 643 | } 644 | 645 | function parseHTML(html) { 646 | return $.parseHTML(html, document, true) 647 | } 648 | 649 | // Internal: Extracts container and metadata from response. 650 | // 651 | // 1. Extracts X-PJAX-URL header if set 652 | // 2. Extracts inline tags 653 | // 3. Builds response Element and extracts fragment if set 654 | // 655 | // data - String response data 656 | // xhr - XHR response 657 | // options - pjax options Object 658 | // 659 | // Returns an Object with url, title, and contents keys. 660 | function extractContainer(data, xhr, options) { 661 | var obj = {}, fullDocument = /<html/i.test(data) 662 | 663 | // Prefer X-PJAX-URL header if it was set, otherwise fallback to 664 | // using the original requested url. 665 | var serverUrl = xhr.getResponseHeader('X-PJAX-URL') 666 | obj.url = serverUrl ? stripInternalParams(parseURL(serverUrl)) : options.requestUrl 667 | 668 | var $head, $body 669 | // Attempt to parse response html into elements 670 | if (fullDocument) { 671 | $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0])) 672 | var head = data.match(/<head[^>]*>([\s\S.]*)<\/head>/i) 673 | $head = head != null ? $(parseHTML(head[0])) : $body 674 | } else { 675 | $head = $body = $(parseHTML(data)) 676 | } 677 | 678 | // If response data is empty, return fast 679 | if ($body.length === 0) 680 | return obj 681 | 682 | // If there's a <title> tag in the header, use it as 683 | // the page's title. 684 | obj.title = findAll($head, 'title').last().text() 685 | 686 | if (options.fragment) { 687 | var $fragment = $body 688 | // If they specified a fragment, look for it in the response 689 | // and pull it out. 690 | if (options.fragment !== 'body') { 691 | $fragment = findAll($fragment, options.fragment).first() 692 | } 693 | 694 | if ($fragment.length) { 695 | obj.contents = options.fragment === 'body' ? $fragment : $fragment.contents() 696 | 697 | // If there's no title, look for data-title and title attributes 698 | // on the fragment 699 | if (!obj.title) 700 | obj.title = $fragment.attr('title') || $fragment.data('title') 701 | } 702 | 703 | } else if (!fullDocument) { 704 | obj.contents = $body 705 | } 706 | 707 | // Clean up any <title> tags 708 | if (obj.contents) { 709 | // Remove any parent title elements 710 | obj.contents = obj.contents.not(function() { return $(this).is('title') }) 711 | 712 | // Then scrub any titles from their descendants 713 | obj.contents.find('title').remove() 714 | 715 | // Gather all script[src] elements 716 | obj.scripts = findAll(obj.contents, 'script[src]').remove() 717 | obj.contents = obj.contents.not(obj.scripts) 718 | } 719 | 720 | // Trim any whitespace off the title 721 | if (obj.title) obj.title = $.trim(obj.title) 722 | 723 | return obj 724 | } 725 | 726 | // Load an execute scripts using standard script request. 727 | // 728 | // Avoids jQuery's traditional $.getScript which does a XHR request and 729 | // globalEval. 730 | // 731 | // scripts - jQuery object of script Elements 732 | // 733 | // Returns nothing. 734 | function executeScriptTags(scripts) { 735 | if (!scripts) return 736 | 737 | var existingScripts = $('script[src]') 738 | 739 | scripts.each(function() { 740 | var src = this.src 741 | var matchedScripts = existingScripts.filter(function() { 742 | return this.src === src 743 | }) 744 | if (matchedScripts.length) return 745 | 746 | var script = document.createElement('script') 747 | var type = $(this).attr('type') 748 | if (type) script.type = type 749 | script.src = $(this).attr('src') 750 | document.head.appendChild(script) 751 | }) 752 | } 753 | 754 | // Internal: History DOM caching class. 755 | var cacheMapping = {} 756 | var cacheForwardStack = [] 757 | var cacheBackStack = [] 758 | 759 | // Push previous state id and container contents into the history 760 | // cache. Should be called in conjunction with `pushState` to save the 761 | // previous container contents. 762 | // 763 | // id - State ID Number 764 | // value - DOM Element to cache 765 | // 766 | // Returns nothing. 767 | function cachePush(id, value) { 768 | cacheMapping[id] = value 769 | cacheBackStack.push(id) 770 | 771 | // Remove all entries in forward history stack after pushing a new page. 772 | trimCacheStack(cacheForwardStack, 0) 773 | 774 | // Trim back history stack to max cache length. 775 | trimCacheStack(cacheBackStack, pjax.defaults.maxCacheLength) 776 | } 777 | 778 | // Shifts cache from directional history cache. Should be 779 | // called on `popstate` with the previous state id and container 780 | // contents. 781 | // 782 | // direction - "forward" or "back" String 783 | // id - State ID Number 784 | // value - DOM Element to cache 785 | // 786 | // Returns nothing. 787 | function cachePop(direction, id, value) { 788 | var pushStack, popStack 789 | cacheMapping[id] = value 790 | 791 | if (direction === 'forward') { 792 | pushStack = cacheBackStack 793 | popStack = cacheForwardStack 794 | } else { 795 | pushStack = cacheForwardStack 796 | popStack = cacheBackStack 797 | } 798 | 799 | pushStack.push(id) 800 | id = popStack.pop() 801 | if (id) delete cacheMapping[id] 802 | 803 | // Trim whichever stack we just pushed to to max cache length. 804 | trimCacheStack(pushStack, pjax.defaults.maxCacheLength) 805 | } 806 | 807 | // Trim a cache stack (either cacheBackStack or cacheForwardStack) to be no 808 | // longer than the specified length, deleting cached DOM elements as necessary. 809 | // 810 | // stack - Array of state IDs 811 | // length - Maximum length to trim to 812 | // 813 | // Returns nothing. 814 | function trimCacheStack(stack, length) { 815 | while (stack.length > length) 816 | delete cacheMapping[stack.shift()] 817 | } 818 | 819 | // Public: Find version identifier for the initial page load. 820 | // 821 | // Returns String version or undefined. 822 | function findVersion() { 823 | return $('meta').filter(function() { 824 | var name = $(this).attr('http-equiv') 825 | return name && name.toUpperCase() === 'X-PJAX-VERSION' 826 | }).attr('content') 827 | } 828 | 829 | // Install pjax functions on $.pjax to enable pushState behavior. 830 | // 831 | // Does nothing if already enabled. 832 | // 833 | // Examples 834 | // 835 | // $.pjax.enable() 836 | // 837 | // Returns nothing. 838 | function enable() { 839 | $.fn.pjax = fnPjax 840 | $.pjax = pjax 841 | $.pjax.enable = $.noop 842 | $.pjax.disable = disable 843 | $.pjax.click = handleClick 844 | $.pjax.submit = handleSubmit 845 | $.pjax.reload = pjaxReload 846 | $.pjax.defaults = { 847 | timeout: 650, 848 | push: true, 849 | replace: false, 850 | type: 'GET', 851 | dataType: 'html', 852 | scrollTo: 0, 853 | maxCacheLength: 20, 854 | version: findVersion 855 | } 856 | $(window).on('popstate.pjax', onPjaxPopstate) 857 | } 858 | 859 | // Disable pushState behavior. 860 | // 861 | // This is the case when a browser doesn't support pushState. It is 862 | // sometimes useful to disable pushState for debugging on a modern 863 | // browser. 864 | // 865 | // Examples 866 | // 867 | // $.pjax.disable() 868 | // 869 | // Returns nothing. 870 | function disable() { 871 | $.fn.pjax = function() { return this } 872 | $.pjax = fallbackPjax 873 | $.pjax.enable = enable 874 | $.pjax.disable = $.noop 875 | $.pjax.click = $.noop 876 | $.pjax.submit = $.noop 877 | $.pjax.reload = function() { window.location.reload() } 878 | 879 | $(window).off('popstate.pjax', onPjaxPopstate) 880 | } 881 | 882 | 883 | // Add the state property to jQuery's event object so we can use it in 884 | // $(window).bind('popstate') 885 | if ($.event.props && $.inArray('state', $.event.props) < 0) { 886 | $.event.props.push('state') 887 | } else if (!('state' in $.Event.prototype)) { 888 | $.event.addProp('state') 889 | } 890 | 891 | // Is pjax supported by this browser? 892 | $.support.pjax = 893 | window.history && window.history.pushState && window.history.replaceState && 894 | // pushState isn't reliable on iOS until 5. 895 | !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/) 896 | 897 | if ($.support.pjax) { 898 | enable() 899 | } else { 900 | disable() 901 | } 902 | 903 | })(jQuery) 904 | --------------------------------------------------------------------------------