You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
916 lines
32 KiB
916 lines
32 KiB
/*! Sea.js 2.2.3 | seajs.org/LICENSE.md */ |
|
!function(a,b){function c(a){return function(b){return{}.toString.call(b)=="[object "+a+"]"}}function d(){return B++}function e(a){return a.match(E)[0]}function f(a){for(a=a.replace(F,"/");a.match(G);)a=a.replace(G,"/");return a=a.replace(H,"$1/")}function g(a){var b=a.length-1,c=a.charAt(b);return"#"===c?a.substring(0,b):".js"===a.substring(b-2)||a.indexOf("?")>0||".css"===a.substring(b-3)||"/"===c?a:a+".js"}function h(a){var b=v.alias;return b&&x(b[a])?b[a]:a}function i(a){var b=v.paths,c;return b&&(c=a.match(I))&&x(b[c[1]])&&(a=b[c[1]]+c[2]),a}function j(a){var b=v.vars;return b&&a.indexOf("{")>-1&&(a=a.replace(J,function(a,c){return x(b[c])?b[c]:a})),a}function k(a){var b=v.map,c=a;if(b)for(var d=0,e=b.length;e>d;d++){var f=b[d];if(c=z(f)?f(a)||a:a.replace(f[0],f[1]),c!==a)break}return c}function l(a,b){var c,d=a.charAt(0);if(K.test(a))c=a;else if("."===d)c=f((b?e(b):v.cwd)+a);else if("/"===d){var g=v.cwd.match(L);c=g?g[0]+a.substring(1):a}else c=v.base+a;return 0===c.indexOf("//")&&(c=location.protocol+c),c}function m(a,b){if(!a)return"";a=h(a),a=i(a),a=j(a),a=g(a);var c=l(a,b);return c=k(c)}function n(a){return a.hasAttribute?a.src:a.getAttribute("src",4)}function o(a,b,c,d){var e=T.test(a),f=M.createElement(e?"link":"script");c&&(f.charset=c),A(d)||f.setAttribute("crossorigin",d),p(f,b,e,a),e?(f.rel="stylesheet",f.href=a):(f.async=!0,f.src=a),U=f,S?R.insertBefore(f,S):R.appendChild(f),U=null}function p(a,c,d,e){function f(){a.onload=a.onerror=a.onreadystatechange=null,d||v.debug||R.removeChild(a),a=null,c()}var g="onload"in a;return!d||!W&&g?(g?(a.onload=f,a.onerror=function(){D("error",{uri:e,node:a}),f()}):a.onreadystatechange=function(){/loaded|complete/.test(a.readyState)&&f()},b):(setTimeout(function(){q(a,c)},1),b)}function q(a,b){var c=a.sheet,d;if(W)c&&(d=!0);else if(c)try{c.cssRules&&(d=!0)}catch(e){"NS_ERROR_DOM_SECURITY_ERR"===e.name&&(d=!0)}setTimeout(function(){d?b():q(a,b)},20)}function r(){if(U)return U;if(V&&"interactive"===V.readyState)return V;for(var a=R.getElementsByTagName("script"),b=a.length-1;b>=0;b--){var c=a[b];if("interactive"===c.readyState)return V=c}}function s(a){var b=[];return a.replace(Y,"").replace(X,function(a,c,d){d&&b.push(d)}),b}function t(a,b){this.uri=a,this.dependencies=b||[],this.exports=null,this.status=0,this._waitings={},this._remain=0}if(!a.seajs){var u=a.seajs={version:"2.2.3"},v=u.data={},w=c("Object"),x=c("String"),y=Array.isArray||c("Array"),z=c("Function"),A=c("Undefined"),B=0,C=v.events={};u.on=function(a,b){var c=C[a]||(C[a]=[]);return c.push(b),u},u.off=function(a,b){if(!a&&!b)return C=v.events={},u;var c=C[a];if(c)if(b)for(var d=c.length-1;d>=0;d--)c[d]===b&&c.splice(d,1);else delete C[a];return u};var D=u.emit=function(a,b){var c=C[a],d;if(c)for(c=c.slice();d=c.shift();)d(b);return u},E=/[^?#]*\//,F=/\/\.\//g,G=/\/[^/]+\/\.\.\//,H=/([^:/])\/\//g,I=/^([^/:]+)(\/.+)$/,J=/{([^{]+)}/g,K=/^\/\/.|:\//,L=/^.*?\/\/.*?\//,M=document,N=e(M.URL),O=M.scripts,P=M.getElementById("seajsnode")||O[O.length-1],Q=e(n(P)||N);u.resolve=m;var R=M.head||M.getElementsByTagName("head")[0]||M.documentElement,S=R.getElementsByTagName("base")[0],T=/\.css(?:\?|$)/i,U,V,W=+navigator.userAgent.replace(/.*(?:AppleWebKit|AndroidWebKit)\/(\d+).*/,"$1")<536;u.request=o;var X=/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g,Y=/\\\\/g,Z=u.cache={},$,_={},ab={},bb={},cb=t.STATUS={FETCHING:1,SAVED:2,LOADING:3,LOADED:4,EXECUTING:5,EXECUTED:6};t.prototype.resolve=function(){for(var a=this,b=a.dependencies,c=[],d=0,e=b.length;e>d;d++)c[d]=t.resolve(b[d],a.uri);return c},t.prototype.load=function(){var a=this;if(!(a.status>=cb.LOADING)){a.status=cb.LOADING;var c=a.resolve();D("load",c);for(var d=a._remain=c.length,e,f=0;d>f;f++)e=t.get(c[f]),e.status<cb.LOADED?e._waitings[a.uri]=(e._waitings[a.uri]||0)+1:a._remain--;if(0===a._remain)return a.onload(),b;var g={};for(f=0;d>f;f++)e=Z[c[f]],e.status<cb.FETCHING?e.fetch(g):e.status===cb.SAVED&&e.load();for(var h in g)g.hasOwnProperty(h)&&g[h]()}},t.prototype.onload=function(){var a=this;a.status=cb.LOADED,a.callback&&a.callback();var b=a._waitings,c,d;for(c in b)b.hasOwnProperty(c)&&(d=Z[c],d._remain-=b[c],0===d._remain&&d.onload());delete a._waitings,delete a._remain},t.prototype.fetch=function(a){function c(){u.request(g.requestUri,g.onRequest,g.charset,g.crossorigin)}function d(){delete _[h],ab[h]=!0,$&&(t.save(f,$),$=null);var a,b=bb[h];for(delete bb[h];a=b.shift();)a.load()}var e=this,f=e.uri;e.status=cb.FETCHING;var g={uri:f};D("fetch",g);var h=g.requestUri||f;return!h||ab[h]?(e.load(),b):_[h]?(bb[h].push(e),b):(_[h]=!0,bb[h]=[e],D("request",g={uri:f,requestUri:h,onRequest:d,charset:z(v.charset)?v.charset(h):v.charset,crossorigin:z(v.crossorigin)?v.crossorigin(h):v.crossorigin}),g.requested||(a?a[g.requestUri]=c:c()),b)},t.prototype.exec=function(){function a(b){return t.get(a.resolve(b)).exec()}var c=this;if(c.status>=cb.EXECUTING)return c.exports;c.status=cb.EXECUTING;var e=c.uri;a.resolve=function(a){return t.resolve(a,e)},a.async=function(b,c){return t.use(b,c,e+"_async_"+d()),a};var f=c.factory,g=z(f)?f(a,c.exports={},c):f;return g===b&&(g=c.exports),delete c.factory,c.exports=g,c.status=cb.EXECUTED,D("exec",c),g},t.resolve=function(a,b){var c={id:a,refUri:b};return D("resolve",c),c.uri||u.resolve(c.id,b)},t.define=function(a,c,d){var e=arguments.length;1===e?(d=a,a=b):2===e&&(d=c,y(a)?(c=a,a=b):c=b),!y(c)&&z(d)&&(c=s(""+d));var f={id:a,uri:t.resolve(a),deps:c,factory:d};if(!f.uri&&M.attachEvent){var g=r();g&&(f.uri=g.src)}D("define",f),f.uri?t.save(f.uri,f):$=f},t.save=function(a,b){var c=t.get(a);c.status<cb.SAVED&&(c.id=b.id||a,c.dependencies=b.deps||[],c.factory=b.factory,c.status=cb.SAVED)},t.get=function(a,b){return Z[a]||(Z[a]=new t(a,b))},t.use=function(b,c,d){var e=t.get(d,y(b)?b:[b]);e.callback=function(){for(var b=[],d=e.resolve(),f=0,g=d.length;g>f;f++)b[f]=Z[d[f]].exec();c&&c.apply(a,b),delete e.callback},e.load()},t.preload=function(a){var b=v.preload,c=b.length;c?t.use(b,function(){b.splice(0,c),t.preload(a)},v.cwd+"_preload_"+d()):a()},u.use=function(a,b){return t.preload(function(){t.use(a,b,v.cwd+"_use_"+d())}),u},t.define.cmd={},a.define=t.define,u.Module=t,v.fetchedList=ab,v.cid=d,u.require=function(a){var b=t.get(t.resolve(a));return b.status<cb.EXECUTING&&(b.onload(),b.exec()),b.exports};var db=/^(.+?\/)(\?\?)?(seajs\/)+/;v.base=(Q.match(db)||["",Q])[1],v.dir=Q,v.cwd=N,v.charset="utf-8",v.preload=function(){var a=[],b=location.search.replace(/(seajs-\w+)(&|$)/g,"$1=1$2");return b+=" "+M.cookie,b.replace(/(seajs-\w+)=1/g,function(b,c){a.push(c)}),a}(),u.config=function(a){for(var b in a){var c=a[b],d=v[b];if(d&&w(d))for(var e in c)d[e]=c[e];else y(d)?c=d.concat(c):"base"===b&&("/"!==c.slice(-1)&&(c+="/"),c=l(c)),v[b]=c}return D("config",a),u}}}(this); |
|
window.require = window.define = window.exports = window.module = undefined; |
|
|
|
/*! |
|
* v2.0.1 |
|
* |
|
* 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 - String selector for the element where to place the response body. |
|
// 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) { |
|
options = optionsFor(container, options); |
|
return this.on('click.pjax', selector, function(event) { |
|
var opts = options; |
|
if (!opts.container) { |
|
opts = $.extend({}, options); |
|
opts.container = $(this).attr('data-pjax') |
|
} |
|
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') |
|
// |
|
// Returns nothing. |
|
function handleClick(event, container, options) { |
|
options = optionsFor(container, options); |
|
|
|
var link = event.currentTarget; |
|
var $link = $(link); |
|
|
|
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) { |
|
// $.pjax.submit(event, '[data-pjax-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 - String selector for where to stick the response body. |
|
// 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 hash = parseURL(options.url).hash; |
|
|
|
var containerType = $.type(options.container); |
|
if (containerType !== 'string') { |
|
throw "expected string value for 'container' option; got " + containerType |
|
} |
|
var context = options.context = $(options.container); |
|
if (!context.length) { |
|
throw "the container selector '" + options.container + "' did not match anything" |
|
} |
|
|
|
// 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: options.container}) |
|
} else { |
|
options.data._pjax = options.container |
|
} |
|
|
|
function fire(type, args, props) { |
|
if (!props) props = {}; |
|
props.relatedTarget = options.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', options.container); |
|
|
|
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: options.container, |
|
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(context, document.activeElement); |
|
|
|
// Clear out any focused controls before inserting new page contents. |
|
if (blurFocus) { |
|
try { |
|
document.activeElement.blur() |
|
} catch (e) { /* ignore */ } |
|
} |
|
|
|
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() |
|
} |
|
|
|
executeStyleTags(container.styles); |
|
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]) |
|
}; |
|
|
|
function executeScriptTags(scripts) { |
|
if (!scripts) return; |
|
|
|
var urls = []; |
|
scripts.each(function() { |
|
urls.unshift($(this).attr('src')); |
|
}); |
|
loadAssets(urls, true); |
|
} |
|
function executeStyleTags(styles) { |
|
if (!styles) return; |
|
|
|
var hrefs = []; |
|
styles.each(function() { |
|
hrefs.unshift($(this).attr('href')); |
|
}); |
|
loadAssets(hrefs); |
|
} |
|
// 按顺序加载静态资源 |
|
function loadAssets(urls, trigger) { |
|
if (urls.length < 1) { |
|
if (trigger) { |
|
fire('pjax:loaded'); |
|
} |
|
return; |
|
} |
|
seajs.use([urls.pop()], function () { |
|
loadAssets(urls, trigger); |
|
}); |
|
} |
|
|
|
// 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: options.container, |
|
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, [options.container, cloneContents(context)]); |
|
|
|
window.history.pushState(null, "", options.requestUrl) |
|
} |
|
|
|
fire('pjax:start', [xhr, options]); |
|
fire('pjax:send', [xhr, options]) |
|
} |
|
|
|
return pjax.xhr |
|
} |
|
|
|
// Public: Reload current page with pjax. |
|
// |
|
// Returns whatever $.pjax returns. |
|
function pjaxReload(container, options) { |
|
var defaults = { |
|
url: window.location.href, |
|
push: false, |
|
replace: true, |
|
scrollTo: false |
|
}; |
|
|
|
return pjax($.extend(defaults, optionsFor(container, options))) |
|
} |
|
|
|
// Internal: Hard replace current state with url. |
|
// |
|
// Work for around WebKit |
|
// https://bugs.webkit.org/show_bug.cgi?id=93506 |
|
// |
|
// Returns nothing. |
|
function locationReplace(url) { |
|
window.history.replaceState(null, "", pjax.state.url); |
|
window.location.replace(url) |
|
} |
|
|
|
|
|
var initialPop = true; |
|
var initialURL = window.location.href; |
|
var initialState = window.history.state; |
|
|
|
// Initialize $.pjax.state if possible |
|
// Happens when reloading a page and coming forward from a different |
|
// session history. |
|
if (initialState && initialState.container) { |
|
pjax.state = initialState |
|
} |
|
|
|
// Non-webkit browsers don't fire an initial popstate event |
|
if ('state' in window.history) { |
|
initialPop = false |
|
} |
|
|
|
// popstate handler takes care of the back and forward buttons |
|
// |
|
// You probably shouldn't use pjax on pages with other pushState |
|
// stuff yet. |
|
function onPjaxPopstate(event) { |
|
|
|
// Hitting back or forward should override any pending PJAX request. |
|
if (!initialPop) { |
|
abortXHR(pjax.xhr) |
|
} |
|
|
|
var previousState = pjax.state; |
|
var state = event.state; |
|
var direction; |
|
|
|
if (state && state.container) { |
|
// When coming forward from a separate history session, will get an |
|
// initial pop with a state we are already at. Skip reloading the current |
|
// page. |
|
if (initialPop && initialURL == state.url) return; |
|
|
|
if (previousState) { |
|
// If popping back to the same state, just skip. |
|
// Could be clicking back from hashchange rather than a pushState. |
|
if (previousState.id === state.id) return; |
|
|
|
// Since state IDs always increase, we can deduce the navigation direction |
|
direction = previousState.id < state.id ? 'forward' : 'back' |
|
} |
|
|
|
var cache = cacheMapping[state.id] || []; |
|
var containerSelector = cache[0] || state.container; |
|
var container = $(containerSelector), 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, [containerSelector, cloneContents(container)]) |
|
} |
|
|
|
var popstateEvent = $.Event('pjax:popstate', { |
|
state: state, |
|
direction: direction |
|
}); |
|
container.trigger(popstateEvent); |
|
|
|
var options = { |
|
id: state.id, |
|
url: state.url, |
|
container: containerSelector, |
|
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 // eslint-disable-line no-unused-expressions |
|
} else { |
|
locationReplace(location.href) |
|
} |
|
} |
|
initialPop = false |
|
} |
|
|
|
// Fallback version of main pjax function for browsers that don't |
|
// support pushState. |
|
// |
|
// Returns nothing since it retriggers a hard form submission. |
|
function fallbackPjax(options) { |
|
var url = $.isFunction(options.url) ? options.url() : options.url, |
|
method = options.type ? options.type.toUpperCase() : 'GET'; |
|
|
|
var form = $('<form>', { |
|
method: method === 'GET' ? 'GET' : 'POST', |
|
action: url, |
|
style: 'display:none' |
|
}); |
|
|
|
if (method !== 'GET' && method !== 'POST') { |
|
form.append($('<input>', { |
|
type: 'hidden', |
|
name: '_method', |
|
value: method.toLowerCase() |
|
})) |
|
} |
|
|
|
var data = options.data; |
|
if (typeof data === 'string') { |
|
$.each(data.split('&'), function(index, value) { |
|
var pair = value.split('='); |
|
form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]})) |
|
}) |
|
} else if ($.isArray(data)) { |
|
$.each(data, function(index, value) { |
|
form.append($('<input>', {type: 'hidden', name: value.name, value: value.value})) |
|
}) |
|
} else if (typeof data === 'object') { |
|
var key; |
|
for (key in data) |
|
form.append($('<input>', {type: 'hidden', name: key, value: data[key]})) |
|
} |
|
|
|
$(document.body).append(form); |
|
form.submit() |
|
} |
|
|
|
// Internal: Abort an XmlHttpRequest if it hasn't been completed, |
|
// also removing its event handlers. |
|
function abortXHR(xhr) { |
|
if ( xhr && xhr.readyState < 4) { |
|
xhr.onreadystatechange = $.noop; |
|
xhr.abort() |
|
} |
|
} |
|
|
|
// Internal: Generate unique id for state object. |
|
// |
|
// Use a timestamp instead of a counter since ids should still be |
|
// unique across page loads. |
|
// |
|
// Returns Number. |
|
function uniqueId() { |
|
return (new Date).getTime() |
|
} |
|
|
|
function cloneContents(container) { |
|
var cloned = container.clone(); |
|
// Unmark script tags as already being eval'd so they can get executed again |
|
// when restored from cache. HAXX: Uses jQuery internal method. |
|
cloned.find('script').each(function(){ |
|
if (!this.src) $._data(this, 'globalEval', false) |
|
}); |
|
return 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, '').replace(/^&/, ''); |
|
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) { |
|
if (container && options) { |
|
options = $.extend({}, options); |
|
options.container = container; |
|
return options |
|
} else if ($.isPlainObject(container)) { |
|
return container |
|
} else { |
|
return {container: 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; |
|
|
|
var $head, $body; |
|
// Attempt to parse response html into elements |
|
if (fullDocument) { |
|
$body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0])); |
|
var head = data.match(/<head[^>]*>([\s\S.]*)<\/head>/i); |
|
$head = head != null ? $(parseHTML(head[0])) : $body |
|
} else { |
|
$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) { |
|
var $fragment = $body; |
|
// If they specified a fragment, look for it in the response |
|
// and pull it out. |
|
if (options.fragment !== 'body') { |
|
$fragment = findAll($fragment, 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); |
|
|
|
// Collect all link[type="text/css"] elements |
|
obj.styles = findAll(obj.contents, 'link[type="text/css"]').remove(); |
|
obj.contents = obj.contents.not(obj.styles); |
|
} |
|
|
|
// Trim any whitespace off the title |
|
if (obj.title) obj.title = $.trim(obj.title); |
|
|
|
return obj |
|
} |
|
|
|
// 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); |
|
id = popStack.pop(); |
|
if (id) 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)/); |
|
|
|
if ($.support.pjax) { |
|
enable() |
|
} else { |
|
disable() |
|
} |
|
|
|
})(jQuery);
|
|
|