jquery.pjax.js
924 lines
| 25.4 KiB
| application/javascript
|
JavascriptLexer
r1 | /*! | ||
* Copyright 2012, Chris Wanstrath | |||
* Released under the MIT License | |||
* https://github.com/defunkt/jquery-pjax | |||
*/ | |||
(function($){ | |||
// When called on a container with a selector, fetches the href with | |||
// ajax into the container 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. | |||
// | |||
// Exported as $.fn.pjax | |||
// | |||
// 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) | |||
// (default: current jquery context) | |||
// push - Whether to pushState the URL. Defaults to true (of course). | |||
// replace - Want to use replaceState instead? That's cool. | |||
// | |||
// For convenience the second parameter can be either the container or | |||
// the options object. | |||
// | |||
// Returns the jQuery object | |||
function fnPjax(selector, container, options) { | |||
var context = this | |||
return this.on('click.pjax', selector, function(event) { | |||
var opts = $.extend({}, optionsFor(container, options)) | |||
if (!opts.container) | |||
opts.container = $(this).attr('data-pjax') || context | |||
handleClick(event, opts) | |||
}) | |||
} | |||
// Public: pjax on click handler | |||
// | |||
// Exported as $.pjax.click. | |||
// | |||
// event - "click" jQuery.Event | |||
// options - pjax options | |||
// | |||
// Examples | |||
// | |||
// $(document).on('click', 'a', $.pjax.click) | |||
// // is the same as | |||
// $(document).pjax('a') | |||
// | |||
// $(document).on('click', 'a', function(event) { | |||
// var container = $(this).closest('[data-pjax-container]') | |||
// $.pjax.click(event, container) | |||
// }) | |||
// | |||
// Returns 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 || event.shiftKey || event.altKey ) | |||
return | |||
// Ignore cross origin links | |||
if ( location.protocol !== link.protocol || location.hostname !== link.hostname ) | |||
return | |||
// Ignore case when a hash is being tacked on the current URL | |||
if ( link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location) ) | |||
return | |||
// Ignore event with default prevented | |||
if (event.isDefaultPrevented()) | |||
return | |||
var defaults = { | |||
url: link.href, | |||
container: $(link).attr('data-pjax'), | |||
target: link | |||
} | |||
var opts = $.extend({}, defaults, options) | |||
var clickEvent = $.Event('pjax:click') | |||
$(link).trigger(clickEvent, [opts]) | |||
if (!clickEvent.isDefaultPrevented()) { | |||
pjax(opts) | |||
event.preventDefault() | |||
$(link).trigger('pjax:clicked', [opts]) | |||
} | |||
} | |||
// Public: pjax on form submit handler | |||
// | |||
// Exported as $.pjax.submit | |||
// | |||
// event - "click" jQuery.Event | |||
// options - pjax options | |||
// | |||
// Examples | |||
// | |||
// $(document).on('submit', 'form', function(event) { | |||
// var container = $(this).closest('[data-pjax-container]') | |||
// $.pjax.submit(event, container) | |||
// }) | |||
// | |||
// Returns nothing. | |||
function handleSubmit(event, container, options) { | |||
options = optionsFor(container, options) | |||
var form = event.currentTarget | |||
var $form = $(form) | |||
if (form.tagName.toUpperCase() !== 'FORM') | |||
throw "$.pjax.submit requires a form element" | |||
var defaults = { | |||
type: ($form.attr('method') || 'GET').toUpperCase(), | |||
url: $form.attr('action'), | |||
container: $form.attr('data-pjax'), | |||
target: form | |||
} | |||
if (defaults.type !== 'GET' && window.FormData !== undefined) { | |||
defaults.data = new FormData(form); | |||
defaults.processData = false; | |||
defaults.contentType = false; | |||
} else { | |||
// Can't handle file uploads, exit | |||
if ($(form).find(':file').length) { | |||
return; | |||
} | |||
// Fallback to manually serializing the fields | |||
defaults.data = $(form).serializeArray(); | |||
} | |||
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. | |||
function pjax(options) { | |||
options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options) | |||
if ($.isFunction(options.url)) { | |||
options.url = options.url() | |||
} | |||
var target = options.target | |||
var hash = parseURL(options.url).hash | |||
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 = {} | |||
if ($.isArray(options.data)) { | |||
options.data.push({name: '_pjax', value: context.selector}) | |||
} else { | |||
options.data._pjax = context.selector | |||
} | |||
function fire(type, args, props) { | |||
if (!props) props = {} | |||
props.relatedTarget = target | |||
var event = $.Event(type, props) | |||
context.trigger(event, args) | |||
return !event.isDefaultPrevented() | |||
} | |||
var timeoutTimer | |||
options.beforeSend = function(xhr, settings) { | |||
// 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 | |||
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 | |||
} | |||
var url = parseURL(settings.url) | |||
if (hash) url.hash = hash | |||
options.requestUrl = stripInternalParams(url) | |||
} | |||
options.complete = function(xhr, textStatus) { | |||
if (timeoutTimer) | |||
clearTimeout(timeoutTimer) | |||
fire('pjax:complete', [xhr, textStatus, options]) | |||
fire('pjax:end', [xhr, options]) | |||
} | |||
options.error = function(xhr, textStatus, errorThrown) { | |||
var container = extractContainer("", xhr, options) | |||
var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]) | |||
if (options.type == 'GET' && textStatus !== 'abort' && allowed) { | |||
locationReplace(container.url) | |||
} | |||
} | |||
options.success = function(data, status, xhr) { | |||
var previousState = pjax.state; | |||
// If $.pjax.defaults.version is a function, invoke it first. | |||
// Otherwise it can be a static string. | |||
var currentVersion = (typeof $.pjax.defaults.version === 'function') ? | |||
$.pjax.defaults.version() : | |||
$.pjax.defaults.version | |||
var latestVersion = xhr.getResponseHeader('X-PJAX-Version') | |||
var container = extractContainer(data, xhr, options) | |||
var url = parseURL(container.url) | |||
if (hash) { | |||
url.hash = hash | |||
container.url = url.href | |||
} | |||
// If there is a layout version mismatch, hard load the new url | |||
if (currentVersion && latestVersion && currentVersion !== latestVersion) { | |||
locationReplace(container.url) | |||
return | |||
} | |||
// If the new response is missing a body, hard load the page | |||
if (!container.contents) { | |||
locationReplace(container.url) | |||
return | |||
} | |||
pjax.state = { | |||
id: options.id || uniqueId(), | |||
url: container.url, | |||
title: container.title, | |||
container: context.selector, | |||
fragment: options.fragment, | |||
timeout: options.timeout | |||
} | |||
if (options.push || options.replace) { | |||
window.history.replaceState(pjax.state, container.title, container.url) | |||
} | |||
// Only blur the focus if the focused element is within the container. | |||
var blurFocus = $.contains(options.container, document.activeElement) | |||
// Clear out any focused controls before inserting new page contents. | |||
if (blurFocus) { | |||
try { | |||
document.activeElement.blur() | |||
} catch (e) { } | |||
} | |||
if (container.title) document.title = container.title | |||
fire('pjax:beforeReplace', [container.contents, options], { | |||
state: pjax.state, | |||
previousState: previousState | |||
}) | |||
context.html(container.contents) | |||
// FF bug: Won't autofocus fields that are inserted via JS. | |||
// This behavior is incorrect. So if theres no current focus, autofocus | |||
// the last field. | |||
// | |||
// http://www.w3.org/html/wg/drafts/html/master/forms.html | |||
var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0] | |||
if (autofocusEl && document.activeElement !== autofocusEl) { | |||
autofocusEl.focus(); | |||
} | |||
executeScriptTags(container.scripts) | |||
var scrollTo = options.scrollTo | |||
// Ensure browser scrolls to the element referenced by the URL anchor | |||
if (hash) { | |||
var name = decodeURIComponent(hash.slice(1)) | |||
var target = document.getElementById(name) || document.getElementsByName(name)[0] | |||
if (target) scrollTo = $(target).offset().top | |||
} | |||
if (typeof scrollTo == 'number') $(window).scrollTop(scrollTo) | |||
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 | |||
} | |||
window.history.replaceState(pjax.state, document.title) | |||
} | |||
// Cancel the current request if we're already pjaxing | |||
abortXHR(pjax.xhr) | |||
pjax.options = options | |||
var xhr = pjax.xhr = $.ajax(options) | |||
if (xhr.readyState > 0) { | |||
if (options.push && !options.replace) { | |||
// Cache current container element before replacing it | |||
cachePush(pjax.state.id, cloneContents(context)) | |||
window.history.pushState(null, "", options.requestUrl) | |||
} | |||
fire('pjax:start', [xhr, options]) | |||
fire('pjax:send', [xhr, options]) | |||
} | |||
return pjax.xhr | |||
} | |||
// Public: Reload current page with pjax. | |||
// | |||
// Returns whatever $.pjax returns. | |||
function pjaxReload(container, options) { | |||
var defaults = { | |||
url: window.location.href, | |||
push: false, | |||
replace: true, | |||
scrollTo: false | |||
} | |||
return pjax($.extend(defaults, optionsFor(container, options))) | |||
} | |||
// Internal: Hard replace current state with url. | |||
// | |||
// Work for around WebKit | |||
// https://bugs.webkit.org/show_bug.cgi?id=93506 | |||
// | |||
// Returns nothing. | |||
function locationReplace(url) { | |||
window.history.replaceState(null, "", pjax.state.url) | |||
window.location.replace(url) | |||
} | |||
var initialPop = true | |||
var initialURL = window.location.href | |||
var initialState = window.history.state | |||
// Initialize $.pjax.state if possible | |||
// Happens when reloading a page and coming forward from a different | |||
// session history. | |||
if (initialState && initialState.container) { | |||
pjax.state = initialState | |||
} | |||
// Non-webkit browsers don't fire an initial popstate event | |||
if ('state' in window.history) { | |||
initialPop = false | |||
} | |||
// popstate handler takes care of the back and forward buttons | |||
// | |||
// You probably shouldn't use pjax on pages with other pushState | |||
// stuff yet. | |||
function onPjaxPopstate(event) { | |||
// Hitting back or forward should override any pending PJAX request. | |||
if (!initialPop) { | |||
abortXHR(pjax.xhr) | |||
} | |||
var previousState = pjax.state | |||
var state = event.state | |||
var direction | |||
if (state && state.container) { | |||
// When coming forward from a separate history session, will get an | |||
// initial pop with a state we are already at. Skip reloading the current | |||
// page. | |||
if (initialPop && initialURL == state.url) return | |||
if (previousState) { | |||
// If popping back to the same state, just skip. | |||
// Could be clicking back from hashchange rather than a pushState. | |||
if (previousState.id === state.id) return | |||
// Since state IDs always increase, we can deduce the navigation direction | |||
direction = previousState.id < state.id ? 'forward' : 'back' | |||
} | |||
var cache = cacheMapping[state.id] || [] | |||
var container = $(cache[0] || state.container), contents = cache[1] | |||
if (container.length) { | |||
if (previousState) { | |||
// Cache current container before replacement and inform the | |||
// cache which direction the history shifted. | |||
cachePop(direction, previousState.id, cloneContents(container)) | |||
} | |||
var popstateEvent = $.Event('pjax:popstate', { | |||
state: state, | |||
direction: direction | |||
}) | |||
container.trigger(popstateEvent) | |||
var options = { | |||
id: state.id, | |||
url: state.url, | |||
container: container, | |||
push: false, | |||
fragment: state.fragment, | |||
timeout: state.timeout, | |||
scrollTo: false | |||
} | |||
if (contents) { | |||
container.trigger('pjax:start', [null, options]) | |||
pjax.state = state | |||
if (state.title) document.title = state.title | |||
var beforeReplaceEvent = $.Event('pjax:beforeReplace', { | |||
state: state, | |||
previousState: previousState | |||
}) | |||
container.trigger(beforeReplaceEvent, [contents, options]) | |||
container.html(contents) | |||
container.trigger('pjax:end', [null, options]) | |||
} else { | |||
pjax(options) | |||
} | |||
// Force reflow/relayout before the browser tries to restore the | |||
// scroll position. | |||
container[0].offsetHeight | |||
} else { | |||
locationReplace(location.href) | |||
} | |||
} | |||
initialPop = false | |||
} | |||
// Fallback version of main pjax function for browsers that don't | |||
// support pushState. | |||
// | |||
// Returns nothing since it retriggers a hard form submission. | |||
function fallbackPjax(options) { | |||
var url = $.isFunction(options.url) ? options.url() : options.url, | |||
method = options.type ? options.type.toUpperCase() : 'GET' | |||
var form = $('<form>', { | |||
method: method === 'GET' ? 'GET' : 'POST', | |||
action: url, | |||
style: 'display:none' | |||
}) | |||
if (method !== 'GET' && method !== 'POST') { | |||
form.append($('<input>', { | |||
type: 'hidden', | |||
name: '_method', | |||
value: method.toLowerCase() | |||
})) | |||
} | |||
var data = options.data | |||
if (typeof data === 'string') { | |||
$.each(data.split('&'), function(index, value) { | |||
var pair = value.split('=') | |||
form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]})) | |||
}) | |||
} else if ($.isArray(data)) { | |||
$.each(data, function(index, value) { | |||
form.append($('<input>', {type: 'hidden', name: value.name, value: value.value})) | |||
}) | |||
} else if (typeof data === 'object') { | |||
var key | |||
for (key in data) | |||
form.append($('<input>', {type: 'hidden', name: key, value: data[key]})) | |||
} | |||
$(document.body).append(form) | |||
form.submit() | |||
} | |||
// Internal: Abort an XmlHttpRequest if it hasn't been completed, | |||
// also removing its event handlers. | |||
function abortXHR(xhr) { | |||
if ( xhr && xhr.readyState < 4) { | |||
xhr.onreadystatechange = $.noop | |||
xhr.abort() | |||
} | |||
} | |||
// 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() | |||
} | |||
function cloneContents(container) { | |||
var cloned = container.clone() | |||
// Unmark script tags as already being eval'd so they can get executed again | |||
// when restored from cache. HAXX: Uses jQuery internal method. | |||
cloned.find('script').each(function(){ | |||
if (!this.src) jQuery._data(this, 'globalEval', false) | |||
}) | |||
return [container.selector, cloned.contents()] | |||
} | |||
// Internal: Strip internal query params from parsed URL. | |||
// | |||
// Returns sanitized url.href String. | |||
function stripInternalParams(url) { | |||
url.search = url.search.replace(/([?&])(_pjax|_)=[^&]*/g, '') | |||
return url.href.replace(/\?($|#)/, '$1') | |||
} | |||
// 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: Return the `href` component of given URL object with the hash | |||
// portion removed. | |||
// | |||
// location - Location or HTMLAnchorElement | |||
// | |||
// Returns String | |||
function stripHash(location) { | |||
return location.href.replace(/#.*/, '') | |||
} | |||
// 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) { | |||
return elems.filter(selector).add(elems.find(selector)); | |||
} | |||
function parseHTML(html) { | |||
return $.parseHTML(html, document, true) | |||
} | |||
// Internal: Extracts container and metadata from response. | |||
// | |||
// 1. Extracts X-PJAX-URL header if set | |||
// 2. Extracts inline <title> tags | |||
// 3. Builds response Element and extracts fragment if set | |||
// | |||
// data - String response data | |||
// xhr - XHR response | |||
// options - pjax options Object | |||
// | |||
// Returns an Object with url, title, and contents keys. | |||
function extractContainer(data, xhr, options) { | |||
var obj = {}, fullDocument = /<html/i.test(data) | |||
// Prefer X-PJAX-URL header if it was set, otherwise fallback to | |||
// using the original requested url. | |||
var serverUrl = xhr.getResponseHeader('X-PJAX-URL') | |||
obj.url = serverUrl ? stripInternalParams(parseURL(serverUrl)) : options.requestUrl | |||
// Attempt to parse response html into elements | |||
if (fullDocument) { | |||
var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0])) | |||
var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0])) | |||
} else { | |||
var $head = $body = $(parseHTML(data)) | |||
} | |||
// If response data is empty, return fast | |||
if ($body.length === 0) | |||
return obj | |||
// If there's a <title> tag in the header, use it as | |||
// the page's title. | |||
obj.title = findAll($head, 'title').last().text() | |||
if (options.fragment) { | |||
// If they specified a fragment, look for it in the response | |||
// and pull it out. | |||
if (options.fragment === 'body') { | |||
var $fragment = $body | |||
} else { | |||
var $fragment = findAll($body, options.fragment).first() | |||
} | |||
if ($fragment.length) { | |||
obj.contents = options.fragment === 'body' ? $fragment : $fragment.contents() | |||
// If there's no title, look for data-title and title attributes | |||
// on the fragment | |||
if (!obj.title) | |||
obj.title = $fragment.attr('title') || $fragment.data('title') | |||
} | |||
} else if (!fullDocument) { | |||
obj.contents = $body | |||
} | |||
// Clean up any <title> tags | |||
if (obj.contents) { | |||
// Remove any parent title elements | |||
obj.contents = obj.contents.not(function() { return $(this).is('title') }) | |||
// Then scrub any titles from their descendants | |||
obj.contents.find('title').remove() | |||
// Gather all script[src] elements | |||
obj.scripts = findAll(obj.contents, 'script[src]').remove() | |||
obj.contents = obj.contents.not(obj.scripts) | |||
} | |||
// Trim any whitespace off the title | |||
if (obj.title) obj.title = $.trim(obj.title) | |||
return obj | |||
} | |||
// Load an execute scripts using standard script request. | |||
// | |||
// Avoids jQuery's traditional $.getScript which does a XHR request and | |||
// globalEval. | |||
// | |||
// scripts - jQuery object of script Elements | |||
// | |||
// Returns nothing. | |||
function executeScriptTags(scripts) { | |||
if (!scripts) return | |||
var existingScripts = $('script[src]') | |||
scripts.each(function() { | |||
var src = this.src | |||
var matchedScripts = existingScripts.filter(function() { | |||
return this.src === src | |||
}) | |||
if (matchedScripts.length) return | |||
var script = document.createElement('script') | |||
var type = $(this).attr('type') | |||
if (type) script.type = type | |||
script.src = $(this).attr('src') | |||
document.head.appendChild(script) | |||
}) | |||
} | |||
// Internal: History DOM caching class. | |||
var cacheMapping = {} | |||
var cacheForwardStack = [] | |||
var cacheBackStack = [] | |||
// Push previous state id and container contents into the history | |||
// cache. Should be called in conjunction with `pushState` to save the | |||
// previous container contents. | |||
// | |||
// id - State ID Number | |||
// value - DOM Element to cache | |||
// | |||
// Returns nothing. | |||
function cachePush(id, value) { | |||
cacheMapping[id] = value | |||
cacheBackStack.push(id) | |||
// Remove all entries in forward history stack after pushing a new page. | |||
trimCacheStack(cacheForwardStack, 0) | |||
// Trim back history stack to max cache length. | |||
trimCacheStack(cacheBackStack, pjax.defaults.maxCacheLength) | |||
} | |||
// Shifts cache from directional history cache. Should be | |||
// called on `popstate` with the previous state id and container | |||
// contents. | |||
// | |||
// direction - "forward" or "back" String | |||
// id - State ID Number | |||
// value - DOM Element to cache | |||
// | |||
// Returns nothing. | |||
function cachePop(direction, id, value) { | |||
var pushStack, popStack | |||
cacheMapping[id] = value | |||
if (direction === 'forward') { | |||
pushStack = cacheBackStack | |||
popStack = cacheForwardStack | |||
} else { | |||
pushStack = cacheForwardStack | |||
popStack = cacheBackStack | |||
} | |||
pushStack.push(id) | |||
if (id = popStack.pop()) | |||
delete cacheMapping[id] | |||
// Trim whichever stack we just pushed to to max cache length. | |||
trimCacheStack(pushStack, pjax.defaults.maxCacheLength) | |||
} | |||
// Trim a cache stack (either cacheBackStack or cacheForwardStack) to be no | |||
// longer than the specified length, deleting cached DOM elements as necessary. | |||
// | |||
// stack - Array of state IDs | |||
// length - Maximum length to trim to | |||
// | |||
// Returns nothing. | |||
function trimCacheStack(stack, length) { | |||
while (stack.length > length) | |||
delete cacheMapping[stack.shift()] | |||
} | |||
// Public: Find version identifier for the initial page load. | |||
// | |||
// Returns String version or undefined. | |||
function findVersion() { | |||
return $('meta').filter(function() { | |||
var name = $(this).attr('http-equiv') | |||
return name && name.toUpperCase() === 'X-PJAX-VERSION' | |||
}).attr('content') | |||
} | |||
// Install pjax functions on $.pjax to enable pushState behavior. | |||
// | |||
// Does nothing if already enabled. | |||
// | |||
// Examples | |||
// | |||
// $.pjax.enable() | |||
// | |||
// Returns nothing. | |||
function enable() { | |||
$.fn.pjax = fnPjax | |||
$.pjax = pjax | |||
$.pjax.enable = $.noop | |||
$.pjax.disable = disable | |||
$.pjax.click = handleClick | |||
$.pjax.submit = handleSubmit | |||
$.pjax.reload = pjaxReload | |||
$.pjax.defaults = { | |||
timeout: 650, | |||
push: true, | |||
replace: false, | |||
type: 'GET', | |||
dataType: 'html', | |||
scrollTo: 0, | |||
maxCacheLength: 20, | |||
version: findVersion | |||
} | |||
$(window).on('popstate.pjax', onPjaxPopstate) | |||
} | |||
// Disable pushState behavior. | |||
// | |||
// This is the case when a browser doesn't support pushState. It is | |||
// sometimes useful to disable pushState for debugging on a modern | |||
// browser. | |||
// | |||
// Examples | |||
// | |||
// $.pjax.disable() | |||
// | |||
// Returns nothing. | |||
function disable() { | |||
$.fn.pjax = function() { return this } | |||
$.pjax = fallbackPjax | |||
$.pjax.enable = enable | |||
$.pjax.disable = $.noop | |||
$.pjax.click = $.noop | |||
$.pjax.submit = $.noop | |||
$.pjax.reload = function() { window.location.reload() } | |||
$(window).off('popstate.pjax', onPjaxPopstate) | |||
} | |||
// 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]\D|WebApps\/.+CFNetwork)/) | |||
$.support.pjax ? enable() : disable() | |||
})(jQuery); |