Skip to content

Instantly share code, notes, and snippets.

@justpsvm

justpsvm/jquery.pjax.2.js

Last active Aug 29, 2017
Embed
What would you like to do?
[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
You can’t perform that action at this time.