// 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);