// zepto.pjax.js // copyright chris wanstrath // https://github.com/defunkt/jquery-pjax // https://github.com/shogun70/jquery-pjax - duration // https://github.com/jimisaacs/zepto-pjax // https://github.com/najeira/zepto-pjax // https://github.com/cshenoy/zepto-pjax - zepto1.0 w/duration (function($){ /** * Add noop to Zepto - should be in there already * It's good to have to only reference one dummy function rather than create multiple empties. */ $.noop = 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 Zepto 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( selector, container, options ) { return this.on('click.pjax', selector, function(event){ handleClick(event, container, options); }); }; // Public: pjax on click handler // // Exported as $.pjax.click. // // event - "click" Zepto.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 || event.ctrlKey ) 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; // Ignore empty anchor "foo.html#" if (link.href === location.href + '#') 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(); } // 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 from handleClick fn options = $.extend({}, $.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 hash = parseURL(options.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); // console.log(options, $.ajaxSettings); //console.log(options, context, context.contents()); // 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) { console.log(' type ',type); var event = $.Event(type, { relatedTarget: target }); context.trigger(event, args); if (event.isDefaultPrevented) { return !event.isDefaultPrevented(); } return !event.defaultPrevented; } var timeoutTimer, durationTimer; var success, complete, error; options.beforeSend = function(xhr, settings) { var timeout = settings.timeout; 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; } // 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; var duration = settings.duration; if (timeoutTimer && duration >= timeout) duration = timeout - 1; if ( (duration >= 0) && (duration != null) ) { durationTimer = setTimeout(function() { durationTimer = null; if (complete) { if (success) success(); if (error) error(); // success and error are mutually exclusive complete(); return; } // otherwise fire the waiting event fire('pjax:waiting', [xhr, options]); }, duration); } if (options.push && !options.replace) { // Cache current container element before replacing it cachePush(pjax.state.id, context.clone(true, true).contents()); window.history.pushState(null, "", options.url); } options.requestUrl = parseURL(settings.url).href; fire('pjax:start', [xhr, options]); // start.pjax is deprecated fire('start.pjax', [xhr, options]); fire('pjax:send', [xhr, settings]); }; options.complete = function(xhr, textStatus){ complete = function() { _complete.call(options, xhr, textStatus); }; if (!durationTimer) complete(); }; function _complete(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) { console.log('error'); var container = extractContainer("", xhr, options); // 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 = container.url; }; options.success = function(data, status, xhr, options) { success = function() { _success.call(options, data, status, xhr) } if (!durationTimer) success(); }; function _success(data, status, xhr) { var container = extractContainer(data, xhr, options); if (!container.contents) { window.location = container.url; return; } pjax.state = { id: options.id || uniqueId(), url: container.url, title: container.title, container: context.selector, fragment: options.fragment, timeout: options.timeout, direction: options.direction, duration: options.duration }; if (options.push || options.replace) { window.history.replaceState(pjax.state, container.title, container.url); } if (container.title) document.title = container.title; context.html(container.contents); // Scroll to top by default if (typeof options.scrollTo === 'number' && $.scrollTop) $(window).scrollTop(options.scrollTo); // 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. 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); } // DEPRECATED: Invoke original `success` handler if (oldSuccess) oldSuccess.apply(this, arguments); 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, direction: options.direction }; 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; pjax.xhr = $.ajax(options); // pjax event is deprecated $(document).trigger('pjax', [pjax.xhr, options]); /*if (xhr.readyState > 0) { // pjax event is deprecated console.log('xhr readystate', xhr.readyState); $(document).trigger('pjax', [xhr, options]); if (options.push && !options.replace) { // Cache current container element before replacing it cachePush(pjax.state.id, context.clone().contents()); window.history.pushState(null, "", options.url); } if(!options.duration) { fire('pjax:start', [xhr, options]); fire('pjax:send', [xhr, options]); } }*/ return pjax.xhr; }; // 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) { var results = $() elems.each(function() { if ($(this).is(selector)) results = results.add(this) results = results.add(selector, this) }) return results } // Internal: Extracts container and metadata from response. // // 1. Extracts X-PJAX-URL header if set // 2. Extracts inline