[pjax jquery 3.0 修复版本] 针对 jquery3.0 的修复版本 不会报 push 错误 #js
/*! | |
* pjax(ajax + history.pushState) for jquery | |
* | |
* by welefen | |
*/ | |
(function($) { | |
var Util = { | |
support : { | |
pjax : window.history && window.history.pushState && window.history.replaceState && !navigator.userAgent.match(/(iPod|iPhone|iPad|WebApps\/.+CFNetwork)/), | |
storage : !!window.localStorage | |
}, | |
toInt : function(obj) { | |
return parseInt(obj); | |
}, | |
stack : {}, | |
getTime : function() { | |
return new Date * 1; | |
}, | |
// 获取URL不带hash的部分,切去掉pjax=true部分 | |
getRealUrl : function(url) { | |
url = (url || '').replace(/\#.*?$/, ''); | |
url = url.replace('?pjax=true&', '?').replace('?pjax=true', '').replace('&pjax=true', ''); | |
return url; | |
}, | |
// 获取url的hash部分 | |
getUrlHash : function(url) { | |
return url.replace(/^[^\#]*(?:\#(.*?))?$/, '$1'); | |
}, | |
// 获取本地存储的key | |
getLocalKey : function(src) { | |
var s = 'pjax_' + encodeURIComponent(src); | |
return { | |
data : s + '_data', | |
time : s + '_time', | |
title : s + '_title' | |
}; | |
}, | |
// 清除所有的cache | |
removeAllCache : function() { | |
if (!Util.support.storage) | |
return; | |
for ( var name in localStorage) { | |
if ((name.split('_') || [ '' ])[0] === 'pjax') { | |
delete localStorage[name]; | |
} | |
} | |
}, | |
// 获取cache | |
getCache : function(src, time, flag) { | |
var item, vkey, tkey, tval; | |
time = Util.toInt(time); | |
if (src in Util.stack) { | |
item = Util.stack[src], ctime = Util.getTime(); | |
if ((item.time + time * 1000) > ctime) { | |
return item; | |
} else { | |
delete Util.stack[src]; | |
} | |
} else if (flag && Util.support.storage) { // 从localStorage里查询 | |
var l = Util.getLocalKey(src); | |
vkey = l.data; | |
tkey = l.time; | |
item = localStorage.getItem(vkey); | |
if (item) { | |
tval = Util.toInt(localStorage.getItem(tkey)); | |
if ((tval + time * 1000) > Util.getTime()) { | |
return { | |
data : item, | |
title : localStorage.getItem(l.title) | |
}; | |
} else { | |
localStorage.removeItem(vkey); | |
localStorage.removeItem(tkey); | |
localStorage.removeItem(l.title); | |
} | |
} | |
} | |
return null; | |
}, | |
// 设置cache | |
setCache : function(src, data, title, flag) { | |
var time = Util.getTime(), key; | |
Util.stack[src] = { | |
data : data, | |
title : title, | |
time : time | |
}; | |
if (flag && Util.support.storage) { | |
key = Util.getLocalKey(src); | |
localStorage.setItem(key.data, data); | |
localStorage.setItem(key.time, time); | |
localStorage.setItem(key.title, title); | |
} | |
}, | |
// 清除cache | |
removeCache : function(src) { | |
src = Util.getRealUrl(src || location.href); | |
delete Util.stack[src]; | |
if (Util.support.storage) { | |
var key = Util.getLocalKey(src); | |
localStorage.removeItem(key.data); | |
localStorage.removeItem(key.time); | |
localStorage.removeItem(key.title); | |
} | |
} | |
}; | |
// pjax | |
var pjax = function(options) { | |
options = $.extend({ | |
selector : '', | |
container : '', | |
callback : function() {}, | |
filter : function() {} | |
}, options); | |
if (!options.container || !options.selector) { | |
throw new Error('selector & container options must be set'); | |
} | |
$('body').delegate(options.selector, 'click', function(event) { | |
if (event.which > 1 || event.metaKey) { | |
return true; | |
} | |
var $this = $(this), href = $this.attr('href'); | |
// 过滤 | |
if (typeof options.filter === 'function') { | |
if (options.filter.call(this, href, this) === true){ | |
return true; | |
} | |
} | |
if (href === location.href) { | |
return true; | |
} | |
// 只是hash不同 | |
if (Util.getRealUrl(href) == Util.getRealUrl(location.href)) { | |
var hash = Util.getUrlHash(href); | |
if (hash) { | |
location.hash = hash; | |
options.callback && options.callback.call(this, { | |
type : 'hash' | |
}); | |
} | |
return true; | |
} | |
event.preventDefault(); | |
options = $.extend(true, options, { | |
url : href, | |
element : this, | |
title: '', | |
push: true | |
}); | |
// 发起请求 | |
pjax.request(options); | |
}); | |
}; | |
pjax.xhr = null; | |
pjax.options = {}; | |
pjax.state = {}; | |
// 默认选项 | |
pjax.defaultOptions = { | |
timeout : 2000, | |
element : null, | |
cache : 24 * 3600, // 缓存时间, 0为不缓存, 单位为秒 | |
storage : true, // 是否使用localstorage将数据保存到本地 | |
url : '', // 链接地址 | |
push : true, // true is push, false is replace, null for do nothing | |
show : '', // 展示的动画 | |
title : '', // 标题 | |
titleSuffix : '',// 标题后缀 | |
type : 'GET', | |
data : { | |
pjax : true | |
}, | |
dataType : 'html', | |
callback : null, // 回调函数 | |
// for jquery | |
beforeSend : function(xhr) { | |
$(pjax.options.container).trigger('pjax.start', [ xhr, pjax.options ]); | |
xhr && xhr.setRequestHeader('X-PJAX', true); | |
}, | |
error : function() { | |
pjax.options.callback && pjax.options.callback.call(pjax.options.element, { | |
type : 'error' | |
}); | |
location.href = pjax.options.url; | |
}, | |
complete : function(xhr) { | |
$(pjax.options.container).trigger('pjax.end', [ xhr, pjax.options ]); | |
} | |
}; | |
// 展现动画 | |
pjax.showFx = { | |
"_default" : function(data, callback, isCached) { | |
this.html(data); | |
callback && callback.call(this, data, isCached); | |
}, | |
fade: function(data, callback, isCached){ | |
var $this = this; | |
if(isCached){ | |
$this.html(data); | |
callback && callback.call($this, data, isCached); | |
}else{ | |
this.fadeOut(500, function(){ | |
$this.html(data).fadeIn(500, function(){ | |
callback && callback.call($this, data, isCached); | |
}); | |
}); | |
} | |
} | |
} | |
// 展现函数 | |
pjax.showFn = function(showType, container, data, fn, isCached) { | |
var fx = null; | |
if (typeof showType === 'function') { | |
fx = showType; | |
} else { | |
if (!(showType in pjax.showFx)) { | |
showType = "_default"; | |
} | |
fx = pjax.showFx[showType]; | |
} | |
fx && fx.call(container, data, function() { | |
var hash = location.hash; | |
if (hash != '') { | |
location.href = hash; | |
//for FF | |
if(/Firefox/.test(navigator.userAgent)){ | |
history.replaceState($.extend({}, pjax.state, { | |
url : null | |
}), document.title); | |
} | |
} else { | |
window.scrollTo(0, 0); | |
} | |
fn && fn.call(this, data, isCached); | |
}, isCached); | |
} | |
// success callback | |
pjax.success = function(data, isCached) { | |
// isCached default is success | |
if (isCached !== true) { | |
isCached = false; | |
} | |
//accept Whole html | |
if (pjax.html) { | |
data = $(data).find(pjax.html).html(); | |
} | |
if ((data || '').indexOf('<html') != -1) { | |
pjax.options.callback && pjax.options.callback.call(pjax.options.element, { | |
type : 'error' | |
}); | |
location.href = pjax.options.url; | |
return false; | |
} | |
var title = pjax.options.title || "", el; | |
if (pjax.options.element) { | |
el = $(pjax.options.element); | |
title = el.attr('title') || el.text(); | |
} | |
var matches = data.match(/<title>(.*?)<\/title>/); | |
if (matches) { | |
title = matches[1]; | |
} | |
if (title) { | |
if (title.indexOf(pjax.options.titleSuffix) == -1) { | |
title += pjax.options.titleSuffix; | |
} | |
} | |
document.title = title; | |
pjax.state = { | |
container : pjax.options.container, | |
timeout : pjax.options.timeout, | |
cache : pjax.options.cache, | |
storage : pjax.options.storage, | |
show : pjax.options.show, | |
title : title, | |
url : pjax.options.oldUrl | |
}; | |
var query = $.param(pjax.options.data); | |
if (query != "") { | |
pjax.state.url = pjax.options.url + (/\?/.test(pjax.options.url) ? "&" : "?") + query; | |
} | |
if (pjax.options.push) { | |
if (!pjax.active) { | |
history.replaceState($.extend({}, pjax.state, { | |
url : null | |
}), document.title); | |
pjax.active = true; | |
} | |
history.pushState(pjax.state, document.title, pjax.options.oldUrl); | |
} else if (pjax.options.push === false) { | |
history.replaceState(pjax.state, document.title, pjax.options.oldUrl); | |
} | |
pjax.options.showFn && pjax.options.showFn(data, function() { | |
pjax.options.callback && pjax.options.callback.call(pjax.options.element,{ | |
type : isCached? 'cache' : 'success' | |
}); | |
}, isCached); | |
// 设置cache | |
if (pjax.options.cache && !isCached) { | |
Util.setCache(pjax.options.url, data, title, pjax.options.storage); | |
} | |
}; | |
// 发送请求 | |
pjax.request = function(options) { | |
options = $.extend(true, pjax.defaultOptions, options); | |
var cache, container = $(options.container); | |
options.oldUrl = options.url; | |
options.url = Util.getRealUrl(options.url); | |
if($(options.element).length){ | |
cache = Util.toInt($(options.element).attr('data-pjax-cache')); | |
if (cache) { | |
options.cache = cache; | |
} | |
} | |
if (options.cache === true) { | |
options.cache = 24 * 3600; | |
} | |
options.cache = Util.toInt(options.cache); | |
// 如果将缓存时间设为0,则将之前的缓存也清除 | |
if (options.cache === 0) { | |
Util.removeAllCache(); | |
} | |
// 展现函数 | |
if (!options.showFn) { | |
options.showFn = function(data, fn, isCached) { | |
pjax.showFn(options.show, container, data, fn, isCached); | |
}; | |
} | |
pjax.options = options; | |
pjax.options.success = pjax.success; | |
if (options.cache && (cache = Util.getCache(options.url, options.cache, options.storage))) { | |
options.beforeSend(); | |
options.title = cache.title; | |
pjax.success(cache.data, true); | |
options.complete(); | |
return true; | |
} | |
if (pjax.xhr && pjax.xhr.readyState < 4) { | |
pjax.xhr.onreadystatechange = $.noop; | |
pjax.xhr.abort(); | |
} | |
pjax.xhr = $.ajax(pjax.options); | |
}; | |
// popstate event | |
var popped = ('state' in window.history), initialURL = location.href; | |
$(window).bind('popstate', function(event) { | |
var initialPop = !popped && location.href == initialURL; | |
popped = true; | |
if (initialPop) return; | |
var state = event.state; | |
if (state && state.container) { | |
if ($(state.container).length) { | |
var data = { | |
url : state.url, | |
container : state.container, | |
push : null, | |
timeout : state.timeout, | |
cache : state.cache, | |
storage : state.storage, | |
title: state.title, | |
element: null | |
}; | |
pjax.request(data); | |
} else { | |
window.location = location.href; | |
} | |
} | |
}); | |
// not support | |
if (!Util.support.pjax) { | |
pjax = function() { | |
return true; | |
}; | |
pjax.request = function(options) { | |
if (options && options.url) { | |
location.href = options.url; | |
} | |
}; | |
} | |
// pjax bind to $ | |
$.pjax = pjax; | |
$.pjax.util = Util; | |
// extra | |
if ( $.event.props && $.inArray('state', $.event.props) < 0 ) { | |
$.event.props.push('state'); | |
} else if ( ! ('state' in $.Event.prototype) ) { | |
$.event.addProp('state'); | |
} | |
})(jQuery); |
/*! | |
* 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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment