|
/*! |
|
* 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 _container = findContainerFor(options.container) |
|
var context = options.context = _container[0] |
|
var selector = _container[1] |
|
|
|
// 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: selector}) |
|
} else { |
|
options.data._pjax = 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', 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: 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: 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, selector)) |
|
|
|
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 selector = cache[0] || state.container |
|
var container = $(selector), 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, selector)) |
|
} |
|
|
|
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, selector) { |
|
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 [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) { |
|
var selector, $container; |
|
if ( $.isArray(container) ) { |
|
$container = container[0] |
|
selector = container[1] |
|
} else { |
|
selector = container |
|
$container = $(selector) |
|
} |
|
|
|
if ( !$container.length ) { |
|
throw "no pjax container for " + selector |
|
} else if ( true ) { |
|
return [$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 ( $.event.props && $.inArray('state', $.event.props) < 0 ) { |
|
$.event.props.push('state'); |
|
} else if ( ! ('state' in $.Event.prototype) ) { |
|
$.event.addProp('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); |