// jquery.pjax.js
// copyright chris wanstrath
// https://github.com/defunkt/jquery-pjax
(function($){
// When called on a link, fetches the href with ajax into the
// container specified as the first parameter 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.
//
// 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)
// push - Whether to pushState the URL. Defaults to true (of course).
// replace - Want to use replaceState instead? That's cool.
//
// For convenience the first parameter can be either the container or
// the options object.
//
// Returns the jQuery object
$.fn.pjax = function( container, options ) {
options = optionsFor(container, options)
return this.live('click', function(event){
return handleClick(event, options)
})
}
// Public: pjax on click handler
//
// Exported as $.pjax.click.
//
// event - "click" jQuery.Event
// options - pjax options
//
// Examples
//
// $('a').live('click', $.pjax.click)
// // is the same as
// $('a').pjax()
//
// $(document).on('click', 'a', function(event) {
// var container = $(this).closest('[data-pjax-container]')
// return $.pjax.click(event, container)
// })
//
// Returns false if pjax runs, otherwise nothing.
function handleClick(event, container, options) {
options = optionsFor(container, options)
var link = event.currentTarget
// Middle click, cmd click, and ctrl click should open
// links in a new tab as normal.
if ( event.which > 1 || event.metaKey )
return
// Ignore cross origin links
if ( location.protocol !== link.protocol || location.host !== link.host )
return
// Ignore anchors on the same page
if ( link.hash && link.href.replace(link.hash, '') ===
location.href.replace(location.hash, '') )
return
var defaults = {
url: link.href,
container: $(link).attr('data-pjax'),
clickedElement: $(link),
fragment: null
}
$.pjax($.extend({}, defaults, options))
event.preventDefault()
return false
}
// 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.
var pjax = $.pjax = function( options ) {
var $container = findContainerFor(options.container),
success = options.success || $.noop
// We don't want to let anyone override our success handler.
delete options.success
options = $.extend(true, {}, pjax.defaults, options)
if ( $.isFunction(options.url) ) {
options.url = options.url()
}
options.context = $container
options.success = function(data){
if ( options.fragment ) {
// If they specified a fragment, look for it in the response
// and pull it out.
var $fragment = $(data).find(options.fragment)
if ( $fragment.length )
data = $fragment.children()
else
return window.location = options.url
} else {
// If we got no data or an entire web page, go directly
// to the page and let normal error handling happen.
if ( !$.trim(data) || / tag in the response, use it as
// the page's title.
var oldTitle = document.title,
title = $.trim( this.find('title').remove().text() )
// No
? Fragment? Look for data-title and title attributes.
if ( !title && options.fragment ) {
title = $fragment.attr('title') || $fragment.data('title')
}
if ( title ) document.title = title
var state = {
pjax: $container.selector,
fragment: options.fragment,
timeout: options.timeout
}
// If there are extra params, save the complete URL in the state object
var query = $.param(options.data)
if ( query != "_pjax=true" )
state.url = options.url + (/\?/.test(options.url) ? "&" : "?") + query
if ( options.replace ) {
pjax.active = true
window.history.replaceState(state, document.title, options.url)
} else if ( options.push ) {
// this extra replaceState before first push ensures good back
// button behavior
if ( !pjax.active ) {
window.history.replaceState($.extend({}, state, {url:null}), oldTitle)
pjax.active = true
}
window.history.pushState(state, document.title, options.url)
}
// Google Analytics support
if ( (options.replace || options.push) && window._gaq )
_gaq.push(['_trackPageview'])
// If the URL has a hash in it, make sure the browser
// knows to navigate to the hash.
var hash = window.location.hash.toString()
if ( hash !== '' ) {
window.location.href = hash
}
// Invoke their success handler if they gave us one.
success.apply(this, arguments)
}
// 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
pjax.xhr = $.ajax(options)
$(document).trigger('pjax', [pjax.xhr, options])
return pjax.xhr
}
// 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!"
}
}
var timeoutTimer = null
pjax.defaults = {
timeout: 650,
push: true,
replace: false,
// 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.
data: { _pjax: true },
type: 'GET',
dataType: 'html',
beforeSend: function(xhr, settings){
var context = this
if (settings.async && settings.timeout > 0) {
timeoutTimer = setTimeout(function() {
var event = $.Event('pjax:timeout')
context.trigger(event, [xhr, pjax.options])
if (event.result !== false)
xhr.abort('timeout')
}, settings.timeout)
// Clear timeout setting so jquerys internal timeout isn't invoked
settings.timeout = 0
}
this.trigger('pjax:start', [xhr, pjax.options])
// start.pjax is deprecated
this.trigger('start.pjax', [xhr, pjax.options])
xhr.setRequestHeader('X-PJAX', 'true')
},
error: function(xhr, textStatus, errorThrown){
if ( textStatus !== 'abort' )
window.location = pjax.options.url
},
complete: function(xhr){
if (timeoutTimer)
clearTimeout(timeoutTimer)
this.trigger('pjax:end', [xhr, pjax.options])
// end.pjax is deprecated
this.trigger('end.pjax', [xhr, pjax.options])
}
}
// Export $.pjax.click
pjax.click = handleClick
// Used to detect initial (useless) popstate.
// If history.state exists, assume browser isn't going to fire initial popstate.
var popped = ('state' in window.history), initialURL = location.href
// popstate handler takes care of the back and forward buttons
//
// You probably shouldn't use pjax on pages with other pushState
// stuff yet.
$(window).bind('popstate', function(event){
// Ignore inital popstate that some browsers fire on page load
var initialPop = !popped && location.href == initialURL
popped = true
if ( initialPop ) return
var state = event.state
if ( state && state.pjax ) {
var container = state.pjax
if ( $(container+'').length )
$.pjax({
url: state.url || location.href,
fragment: state.fragment,
container: container,
push: false,
timeout: state.timeout
})
else
window.location = location.href
}
})
// 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)/)
// Fall back to normalcy for older browsers.
if ( !$.support.pjax ) {
$.pjax = function( options ) {
window.location = $.isFunction(options.url) ? options.url() : options.url
}
$.pjax.click = $.noop
$.fn.pjax = function() { return this }
}
})(jQuery);