// 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 ) { return this.live('click', function(event){ return handleClick(event, container, 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 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 ) 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'), target: link, clickedElement: $(link), // DEPRECATED: use target fragment: null } $.pjax($.extend({}, defaults, options)) event.preventDefault() return false } // 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 } // 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 ) { options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options) if ($.isFunction(options.url)) { options.url = options.url() } var target = options.target // DEPRECATED: use options.target if (!target && options.clickedElement) target = options.clickedElement[0] var url = options.url var hash = parseURL(url).hash // DEPRECATED: Save references to original event callbacks. However, // listening for custom pjax:* events is prefered. var oldBeforeSend = options.beforeSend, oldComplete = options.complete, oldSuccess = options.success, oldError = options.error 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) { url = stripPjaxParam(settings.url) 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 } xhr.setRequestHeader('X-PJAX', 'true') xhr.setRequestHeader('X-PJAX-Container', context.selector) var result // DEPRECATED: Invoke original `beforeSend` handler if (oldBeforeSend) { result = oldBeforeSend.apply(this, arguments) if (result === false) return false } if (!fire('pjax:beforeSend', [xhr, settings])) return false fire('pjax:start', [xhr, options]) // start.pjax is deprecated fire('start.pjax', [xhr, options]) } options.complete = function(xhr, textStatus) { if (timeoutTimer) clearTimeout(timeoutTimer) // DEPRECATED: Invoke original `complete` handler if (oldComplete) oldComplete.apply(this, arguments) fire('pjax:complete', [xhr, textStatus, options]) fire('pjax:end', [xhr, options]) // end.pjax is deprecated fire('end.pjax', [xhr, options]) } options.error = function(xhr, textStatus, errorThrown) { var respUrl = xhr.getResponseHeader('X-PJAX-URL') if (respUrl) url = stripPjaxParam(respUrl) // DEPRECATED: Invoke original `error` handler if (oldError) oldError.apply(this, arguments) var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]) if (textStatus !== 'abort' && allowed) window.location = url } options.success = function(data, status, xhr) { var respUrl = xhr.getResponseHeader('X-PJAX-URL') if (respUrl) url = stripPjaxParam(respUrl) var title, oldTitle = document.title if ( options.fragment ) { // If they specified a fragment, look for it in the response // and pull it out. var html = $('').html(data) var $fragment = html.find(options.fragment) if ( $fragment.length ) { this.html($fragment.contents()) // If there's a