jquery.pjax.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  1. /*! Sea.js 2.2.3 | seajs.org/LICENSE.md */
  2. !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);
  3. window.require = window.define = window.exports = window.module = undefined;
  4. /*!
  5. * v2.0.1
  6. *
  7. * Copyright 2012, Chris Wanstrath
  8. * Released under the MIT License
  9. * https://github.com/defunkt/jquery-pjax
  10. */
  11. (function($){
  12. // When called on a container with a selector, fetches the href with
  13. // ajax into the container or with the data-pjax attribute on the link
  14. // itself.
  15. //
  16. // Tries to make sure the back button and ctrl+click work the way
  17. // you'd expect.
  18. //
  19. // Exported as $.fn.pjax
  20. //
  21. // Accepts a jQuery ajax options object that may include these
  22. // pjax specific options:
  23. //
  24. //
  25. // container - String selector for the element where to place the response body.
  26. // push - Whether to pushState the URL. Defaults to true (of course).
  27. // replace - Want to use replaceState instead? That's cool.
  28. //
  29. // For convenience the second parameter can be either the container or
  30. // the options object.
  31. //
  32. // Returns the jQuery object
  33. function fnPjax(selector, container, options) {
  34. options = optionsFor(container, options);
  35. return this.on('click.pjax', selector, function(event) {
  36. var opts = options;
  37. if (!opts.container) {
  38. opts = $.extend({}, options);
  39. opts.container = $(this).attr('data-pjax')
  40. }
  41. handleClick(event, opts)
  42. })
  43. }
  44. // Public: pjax on click handler
  45. //
  46. // Exported as $.pjax.click.
  47. //
  48. // event - "click" jQuery.Event
  49. // options - pjax options
  50. //
  51. // Examples
  52. //
  53. // $(document).on('click', 'a', $.pjax.click)
  54. // // is the same as
  55. // $(document).pjax('a')
  56. //
  57. // Returns nothing.
  58. function handleClick(event, container, options) {
  59. options = optionsFor(container, options);
  60. var link = event.currentTarget;
  61. var $link = $(link);
  62. if (link.tagName.toUpperCase() !== 'A')
  63. throw "$.fn.pjax or $.pjax.click requires an anchor element";
  64. // Middle click, cmd click, and ctrl click should open
  65. // links in a new tab as normal.
  66. if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )
  67. return;
  68. // Ignore cross origin links
  69. if ( location.protocol !== link.protocol || location.hostname !== link.hostname )
  70. return;
  71. // Ignore case when a hash is being tacked on the current URL
  72. if ( link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location) )
  73. return;
  74. // Ignore event with default prevented
  75. if (event.isDefaultPrevented())
  76. return;
  77. var defaults = {
  78. url: link.href,
  79. container: $link.attr('data-pjax'),
  80. target: link
  81. };
  82. var opts = $.extend({}, defaults, options);
  83. var clickEvent = $.Event('pjax:click');
  84. $link.trigger(clickEvent, [opts]);
  85. if (!clickEvent.isDefaultPrevented()) {
  86. pjax(opts);
  87. event.preventDefault();
  88. $link.trigger('pjax:clicked', [opts])
  89. }
  90. }
  91. // Public: pjax on form submit handler
  92. //
  93. // Exported as $.pjax.submit
  94. //
  95. // event - "click" jQuery.Event
  96. // options - pjax options
  97. //
  98. // Examples
  99. //
  100. // $(document).on('submit', 'form', function(event) {
  101. // $.pjax.submit(event, '[data-pjax-container]')
  102. // })
  103. //
  104. // Returns nothing.
  105. function handleSubmit(event, container, options) {
  106. options = optionsFor(container, options);
  107. var form = event.currentTarget;
  108. var $form = $(form);
  109. if (form.tagName.toUpperCase() !== 'FORM')
  110. throw "$.pjax.submit requires a form element";
  111. var defaults = {
  112. type: ($form.attr('method') || 'GET').toUpperCase(),
  113. url: $form.attr('action'),
  114. container: $form.attr('data-pjax'),
  115. target: form
  116. };
  117. if (defaults.type !== 'GET' && window.FormData !== undefined) {
  118. defaults.data = new FormData(form);
  119. defaults.processData = false;
  120. defaults.contentType = false;
  121. } else {
  122. // Can't handle file uploads, exit
  123. if ($form.find(':file').length) {
  124. return
  125. }
  126. // Fallback to manually serializing the fields
  127. defaults.data = $form.serializeArray()
  128. }
  129. pjax($.extend({}, defaults, options));
  130. event.preventDefault()
  131. }
  132. // Loads a URL with ajax, puts the response body inside a container,
  133. // then pushState()'s the loaded URL.
  134. //
  135. // Works just like $.ajax in that it accepts a jQuery ajax
  136. // settings object (with keys like url, type, data, etc).
  137. //
  138. // Accepts these extra keys:
  139. //
  140. // container - String selector for where to stick the response body.
  141. // push - Whether to pushState the URL. Defaults to true (of course).
  142. // replace - Want to use replaceState instead? That's cool.
  143. //
  144. // Use it just like $.ajax:
  145. //
  146. // var xhr = $.pjax({ url: this.href, container: '#main' })
  147. // console.log( xhr.readyState )
  148. //
  149. // Returns whatever $.ajax returns.
  150. function pjax(options) {
  151. options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options);
  152. if ($.isFunction(options.url)) {
  153. options.url = options.url()
  154. }
  155. var hash = parseURL(options.url).hash;
  156. var containerType = $.type(options.container);
  157. if (containerType !== 'string') {
  158. throw "expected string value for 'container' option; got " + containerType
  159. }
  160. var context = options.context = $(options.container);
  161. if (!context.length) {
  162. throw "the container selector '" + options.container + "' did not match anything"
  163. }
  164. // We want the browser to maintain two separate internal caches: one
  165. // for pjax'd partial page loads and one for normal page loads.
  166. // Without adding this secret parameter, some browsers will often
  167. // confuse the two.
  168. if (!options.data) options.data = {};
  169. if ($.isArray(options.data)) {
  170. options.data.push({name: '_pjax', value: options.container})
  171. } else {
  172. options.data._pjax = options.container
  173. }
  174. function fire(type, args, props) {
  175. if (!props) props = {};
  176. props.relatedTarget = options.target;
  177. var event = $.Event(type, props);
  178. context.trigger(event, args);
  179. return !event.isDefaultPrevented()
  180. }
  181. var timeoutTimer;
  182. options.beforeSend = function(xhr, settings) {
  183. // No timeout for non-GET requests
  184. // Its not safe to request the resource again with a fallback method.
  185. if (settings.type !== 'GET') {
  186. settings.timeout = 0
  187. }
  188. xhr.setRequestHeader('X-PJAX', 'true');
  189. xhr.setRequestHeader('X-PJAX-Container', options.container);
  190. if (!fire('pjax:beforeSend', [xhr, settings]))
  191. return false;
  192. if (settings.timeout > 0) {
  193. timeoutTimer = setTimeout(function() {
  194. if (fire('pjax:timeout', [xhr, options]))
  195. xhr.abort('timeout')
  196. }, settings.timeout);
  197. // Clear timeout setting so jquerys internal timeout isn't invoked
  198. settings.timeout = 0
  199. }
  200. var url = parseURL(settings.url);
  201. if (hash) url.hash = hash;
  202. options.requestUrl = stripInternalParams(url)
  203. };
  204. options.complete = function(xhr, textStatus) {
  205. if (timeoutTimer)
  206. clearTimeout(timeoutTimer);
  207. fire('pjax:complete', [xhr, textStatus, options]);
  208. fire('pjax:end', [xhr, options])
  209. };
  210. options.error = function(xhr, textStatus, errorThrown) {
  211. var container = extractContainer("", xhr, options);
  212. var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]);
  213. if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
  214. locationReplace(container.url)
  215. }
  216. };
  217. options.success = function(data, status, xhr) {
  218. var previousState = pjax.state;
  219. // If $.pjax.defaults.version is a function, invoke it first.
  220. // Otherwise it can be a static string.
  221. var currentVersion = typeof $.pjax.defaults.version === 'function' ?
  222. $.pjax.defaults.version() :
  223. $.pjax.defaults.version;
  224. var latestVersion = xhr.getResponseHeader('X-PJAX-Version');
  225. var container = extractContainer(data, xhr, options);
  226. var url = parseURL(container.url);
  227. if (hash) {
  228. url.hash = hash;
  229. container.url = url.href
  230. }
  231. // If there is a layout version mismatch, hard load the new url
  232. if (currentVersion && latestVersion && currentVersion !== latestVersion) {
  233. locationReplace(container.url);
  234. return
  235. }
  236. // If the new response is missing a body, hard load the page
  237. if (!container.contents) {
  238. locationReplace(container.url);
  239. return
  240. }
  241. pjax.state = {
  242. id: options.id || uniqueId(),
  243. url: container.url,
  244. title: container.title,
  245. container: options.container,
  246. fragment: options.fragment,
  247. timeout: options.timeout
  248. };
  249. if (options.push || options.replace) {
  250. window.history.replaceState(pjax.state, container.title, container.url)
  251. }
  252. // Only blur the focus if the focused element is within the container.
  253. var blurFocus = $.contains(context, document.activeElement);
  254. // Clear out any focused controls before inserting new page contents.
  255. if (blurFocus) {
  256. try {
  257. document.activeElement.blur()
  258. } catch (e) { /* ignore */ }
  259. }
  260. if (container.title) document.title = container.title;
  261. fire('pjax:beforeReplace', [container.contents, options], {
  262. state: pjax.state,
  263. previousState: previousState
  264. });
  265. context.html(container.contents);
  266. // FF bug: Won't autofocus fields that are inserted via JS.
  267. // This behavior is incorrect. So if theres no current focus, autofocus
  268. // the last field.
  269. //
  270. // http://www.w3.org/html/wg/drafts/html/master/forms.html
  271. var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0];
  272. if (autofocusEl && document.activeElement !== autofocusEl) {
  273. autofocusEl.focus()
  274. }
  275. executeStyleTags(container.styles);
  276. executeScriptTags(container.scripts);
  277. var scrollTo = options.scrollTo;
  278. // Ensure browser scrolls to the element referenced by the URL anchor
  279. if (hash) {
  280. var name = decodeURIComponent(hash.slice(1));
  281. var target = document.getElementById(name) || document.getElementsByName(name)[0];
  282. if (target) scrollTo = $(target).offset().top
  283. }
  284. if (typeof scrollTo == 'number') $(window).scrollTop(scrollTo);
  285. fire('pjax:success', [data, status, xhr, options])
  286. };
  287. function executeScriptTags(scripts) {
  288. if (!scripts) return;
  289. var urls = [];
  290. scripts.each(function() {
  291. urls.unshift($(this).attr('src'));
  292. });
  293. loadAssets(urls, true);
  294. }
  295. function executeStyleTags(styles) {
  296. if (!styles) return;
  297. var hrefs = [];
  298. styles.each(function() {
  299. hrefs.unshift($(this).attr('href'));
  300. });
  301. loadAssets(hrefs);
  302. }
  303. // 按顺序加载静态资源
  304. function loadAssets(urls, trigger) {
  305. if (urls.length < 1) {
  306. if (trigger) {
  307. fire('pjax:loaded');
  308. }
  309. return;
  310. }
  311. seajs.use([urls.pop()], function () {
  312. loadAssets(urls, trigger);
  313. });
  314. }
  315. // Initialize pjax.state for the initial page load. Assume we're
  316. // using the container and options of the link we're loading for the
  317. // back button to the initial page. This ensures good back button
  318. // behavior.
  319. if (!pjax.state) {
  320. pjax.state = {
  321. id: uniqueId(),
  322. url: window.location.href,
  323. title: document.title,
  324. container: options.container,
  325. fragment: options.fragment,
  326. timeout: options.timeout
  327. };
  328. window.history.replaceState(pjax.state, document.title)
  329. }
  330. // Cancel the current request if we're already pjaxing
  331. abortXHR(pjax.xhr);
  332. pjax.options = options;
  333. var xhr = pjax.xhr = $.ajax(options);
  334. if (xhr.readyState > 0) {
  335. if (options.push && !options.replace) {
  336. // Cache current container element before replacing it
  337. cachePush(pjax.state.id, [options.container, cloneContents(context)]);
  338. window.history.pushState(null, "", options.requestUrl)
  339. }
  340. fire('pjax:start', [xhr, options]);
  341. fire('pjax:send', [xhr, options])
  342. }
  343. return pjax.xhr
  344. }
  345. // Public: Reload current page with pjax.
  346. //
  347. // Returns whatever $.pjax returns.
  348. function pjaxReload(container, options) {
  349. var defaults = {
  350. url: window.location.href,
  351. push: false,
  352. replace: true,
  353. scrollTo: false
  354. };
  355. return pjax($.extend(defaults, optionsFor(container, options)))
  356. }
  357. // Internal: Hard replace current state with url.
  358. //
  359. // Work for around WebKit
  360. // https://bugs.webkit.org/show_bug.cgi?id=93506
  361. //
  362. // Returns nothing.
  363. function locationReplace(url) {
  364. window.history.replaceState(null, "", pjax.state.url);
  365. window.location.replace(url)
  366. }
  367. var initialPop = true;
  368. var initialURL = window.location.href;
  369. var initialState = window.history.state;
  370. // Initialize $.pjax.state if possible
  371. // Happens when reloading a page and coming forward from a different
  372. // session history.
  373. if (initialState && initialState.container) {
  374. pjax.state = initialState
  375. }
  376. // Non-webkit browsers don't fire an initial popstate event
  377. if ('state' in window.history) {
  378. initialPop = false
  379. }
  380. // popstate handler takes care of the back and forward buttons
  381. //
  382. // You probably shouldn't use pjax on pages with other pushState
  383. // stuff yet.
  384. function onPjaxPopstate(event) {
  385. // Hitting back or forward should override any pending PJAX request.
  386. if (!initialPop) {
  387. abortXHR(pjax.xhr)
  388. }
  389. var previousState = pjax.state;
  390. var state = event.state;
  391. var direction;
  392. if (state && state.container) {
  393. // When coming forward from a separate history session, will get an
  394. // initial pop with a state we are already at. Skip reloading the current
  395. // page.
  396. if (initialPop && initialURL == state.url) return;
  397. if (previousState) {
  398. // If popping back to the same state, just skip.
  399. // Could be clicking back from hashchange rather than a pushState.
  400. if (previousState.id === state.id) return;
  401. // Since state IDs always increase, we can deduce the navigation direction
  402. direction = previousState.id < state.id ? 'forward' : 'back'
  403. }
  404. var cache = cacheMapping[state.id] || [];
  405. var containerSelector = cache[0] || state.container;
  406. var container = $(containerSelector), contents = cache[1];
  407. if (container.length) {
  408. if (previousState) {
  409. // Cache current container before replacement and inform the
  410. // cache which direction the history shifted.
  411. cachePop(direction, previousState.id, [containerSelector, cloneContents(container)])
  412. }
  413. var popstateEvent = $.Event('pjax:popstate', {
  414. state: state,
  415. direction: direction
  416. });
  417. container.trigger(popstateEvent);
  418. var options = {
  419. id: state.id,
  420. url: state.url,
  421. container: containerSelector,
  422. push: false,
  423. fragment: state.fragment,
  424. timeout: state.timeout,
  425. scrollTo: false
  426. };
  427. if (contents) {
  428. container.trigger('pjax:start', [null, options]);
  429. pjax.state = state;
  430. if (state.title) document.title = state.title;
  431. var beforeReplaceEvent = $.Event('pjax:beforeReplace', {
  432. state: state,
  433. previousState: previousState
  434. });
  435. container.trigger(beforeReplaceEvent, [contents, options]);
  436. container.html(contents);
  437. container.trigger('pjax:end', [null, options])
  438. } else {
  439. pjax(options)
  440. }
  441. // Force reflow/relayout before the browser tries to restore the
  442. // scroll position.
  443. container[0].offsetHeight // eslint-disable-line no-unused-expressions
  444. } else {
  445. locationReplace(location.href)
  446. }
  447. }
  448. initialPop = false
  449. }
  450. // Fallback version of main pjax function for browsers that don't
  451. // support pushState.
  452. //
  453. // Returns nothing since it retriggers a hard form submission.
  454. function fallbackPjax(options) {
  455. var url = $.isFunction(options.url) ? options.url() : options.url,
  456. method = options.type ? options.type.toUpperCase() : 'GET';
  457. var form = $('<form>', {
  458. method: method === 'GET' ? 'GET' : 'POST',
  459. action: url,
  460. style: 'display:none'
  461. });
  462. if (method !== 'GET' && method !== 'POST') {
  463. form.append($('<input>', {
  464. type: 'hidden',
  465. name: '_method',
  466. value: method.toLowerCase()
  467. }))
  468. }
  469. var data = options.data;
  470. if (typeof data === 'string') {
  471. $.each(data.split('&'), function(index, value) {
  472. var pair = value.split('=');
  473. form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
  474. })
  475. } else if ($.isArray(data)) {
  476. $.each(data, function(index, value) {
  477. form.append($('<input>', {type: 'hidden', name: value.name, value: value.value}))
  478. })
  479. } else if (typeof data === 'object') {
  480. var key;
  481. for (key in data)
  482. form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
  483. }
  484. $(document.body).append(form);
  485. form.submit()
  486. }
  487. // Internal: Abort an XmlHttpRequest if it hasn't been completed,
  488. // also removing its event handlers.
  489. function abortXHR(xhr) {
  490. if ( xhr && xhr.readyState < 4) {
  491. xhr.onreadystatechange = $.noop;
  492. xhr.abort()
  493. }
  494. }
  495. // Internal: Generate unique id for state object.
  496. //
  497. // Use a timestamp instead of a counter since ids should still be
  498. // unique across page loads.
  499. //
  500. // Returns Number.
  501. function uniqueId() {
  502. return (new Date).getTime()
  503. }
  504. function cloneContents(container) {
  505. var cloned = container.clone();
  506. // Unmark script tags as already being eval'd so they can get executed again
  507. // when restored from cache. HAXX: Uses jQuery internal method.
  508. cloned.find('script').each(function(){
  509. if (!this.src) $._data(this, 'globalEval', false)
  510. });
  511. return cloned.contents()
  512. }
  513. // Internal: Strip internal query params from parsed URL.
  514. //
  515. // Returns sanitized url.href String.
  516. function stripInternalParams(url) {
  517. url.search = url.search.replace(/([?&])(_pjax|_)=[^&]*/g, '').replace(/^&/, '');
  518. return url.href.replace(/\?($|#)/, '$1')
  519. }
  520. // Internal: Parse URL components and returns a Locationish object.
  521. //
  522. // url - String URL
  523. //
  524. // Returns HTMLAnchorElement that acts like Location.
  525. function parseURL(url) {
  526. var a = document.createElement('a');
  527. a.href = url;
  528. return a
  529. }
  530. // Internal: Return the `href` component of given URL object with the hash
  531. // portion removed.
  532. //
  533. // location - Location or HTMLAnchorElement
  534. //
  535. // Returns String
  536. function stripHash(location) {
  537. return location.href.replace(/#.*/, '')
  538. }
  539. // Internal: Build options Object for arguments.
  540. //
  541. // For convenience the first parameter can be either the container or
  542. // the options object.
  543. //
  544. // Examples
  545. //
  546. // optionsFor('#container')
  547. // // => {container: '#container'}
  548. //
  549. // optionsFor('#container', {push: true})
  550. // // => {container: '#container', push: true}
  551. //
  552. // optionsFor({container: '#container', push: true})
  553. // // => {container: '#container', push: true}
  554. //
  555. // Returns options Object.
  556. function optionsFor(container, options) {
  557. if (container && options) {
  558. options = $.extend({}, options);
  559. options.container = container;
  560. return options
  561. } else if ($.isPlainObject(container)) {
  562. return container
  563. } else {
  564. return {container: container}
  565. }
  566. }
  567. // Internal: Filter and find all elements matching the selector.
  568. //
  569. // Where $.fn.find only matches descendants, findAll will test all the
  570. // top level elements in the jQuery object as well.
  571. //
  572. // elems - jQuery object of Elements
  573. // selector - String selector to match
  574. //
  575. // Returns a jQuery object.
  576. function findAll(elems, selector) {
  577. return elems.filter(selector).add(elems.find(selector))
  578. }
  579. function parseHTML(html) {
  580. return $.parseHTML(html, document, true)
  581. }
  582. // Internal: Extracts container and metadata from response.
  583. //
  584. // 1. Extracts X-PJAX-URL header if set
  585. // 2. Extracts inline <title> tags
  586. // 3. Builds response Element and extracts fragment if set
  587. //
  588. // data - String response data
  589. // xhr - XHR response
  590. // options - pjax options Object
  591. //
  592. // Returns an Object with url, title, and contents keys.
  593. function extractContainer(data, xhr, options) {
  594. var obj = {}, fullDocument = /<html/i.test(data);
  595. // Prefer X-PJAX-URL header if it was set, otherwise fallback to
  596. // using the original requested url.
  597. var serverUrl = xhr.getResponseHeader('X-PJAX-URL');
  598. obj.url = serverUrl ? stripInternalParams(parseURL(serverUrl)) : options.requestUrl;
  599. var $head, $body;
  600. // Attempt to parse response html into elements
  601. if (fullDocument) {
  602. $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]));
  603. var head = data.match(/<head[^>]*>([\s\S.]*)<\/head>/i);
  604. $head = head != null ? $(parseHTML(head[0])) : $body
  605. } else {
  606. $head = $body = $(parseHTML(data))
  607. }
  608. // If response data is empty, return fast
  609. if ($body.length === 0)
  610. return obj;
  611. // If there's a <title> tag in the header, use it as
  612. // the page's title.
  613. obj.title = findAll($head, 'title').last().text();
  614. if (options.fragment) {
  615. var $fragment = $body;
  616. // If they specified a fragment, look for it in the response
  617. // and pull it out.
  618. if (options.fragment !== 'body') {
  619. $fragment = findAll($fragment, options.fragment).first()
  620. }
  621. if ($fragment.length) {
  622. obj.contents = options.fragment === 'body' ? $fragment : $fragment.contents();
  623. // If there's no title, look for data-title and title attributes
  624. // on the fragment
  625. if (!obj.title)
  626. obj.title = $fragment.attr('title') || $fragment.data('title')
  627. }
  628. } else if (!fullDocument) {
  629. obj.contents = $body
  630. }
  631. // Clean up any <title> tags
  632. if (obj.contents) {
  633. // Remove any parent title elements
  634. obj.contents = obj.contents.not(function() { return $(this).is('title') });
  635. // Then scrub any titles from their descendants
  636. obj.contents.find('title').remove();
  637. // Gather all script[src] elements
  638. obj.scripts = findAll(obj.contents, 'script[src]').remove();
  639. obj.contents = obj.contents.not(obj.scripts);
  640. // Collect all link[type="text/css"] elements
  641. obj.styles = findAll(obj.contents, 'link[type="text/css"]').remove();
  642. obj.contents = obj.contents.not(obj.styles);
  643. }
  644. // Trim any whitespace off the title
  645. if (obj.title) obj.title = $.trim(obj.title);
  646. return obj
  647. }
  648. // Internal: History DOM caching class.
  649. var cacheMapping = {};
  650. var cacheForwardStack = [];
  651. var cacheBackStack = [];
  652. // Push previous state id and container contents into the history
  653. // cache. Should be called in conjunction with `pushState` to save the
  654. // previous container contents.
  655. //
  656. // id - State ID Number
  657. // value - DOM Element to cache
  658. //
  659. // Returns nothing.
  660. function cachePush(id, value) {
  661. cacheMapping[id] = value;
  662. cacheBackStack.push(id);
  663. // Remove all entries in forward history stack after pushing a new page.
  664. trimCacheStack(cacheForwardStack, 0);
  665. // Trim back history stack to max cache length.
  666. trimCacheStack(cacheBackStack, pjax.defaults.maxCacheLength)
  667. }
  668. // Shifts cache from directional history cache. Should be
  669. // called on `popstate` with the previous state id and container
  670. // contents.
  671. //
  672. // direction - "forward" or "back" String
  673. // id - State ID Number
  674. // value - DOM Element to cache
  675. //
  676. // Returns nothing.
  677. function cachePop(direction, id, value) {
  678. var pushStack, popStack;
  679. cacheMapping[id] = value;
  680. if (direction === 'forward') {
  681. pushStack = cacheBackStack;
  682. popStack = cacheForwardStack
  683. } else {
  684. pushStack = cacheForwardStack;
  685. popStack = cacheBackStack
  686. }
  687. pushStack.push(id);
  688. id = popStack.pop();
  689. if (id) delete cacheMapping[id];
  690. // Trim whichever stack we just pushed to to max cache length.
  691. trimCacheStack(pushStack, pjax.defaults.maxCacheLength)
  692. }
  693. // Trim a cache stack (either cacheBackStack or cacheForwardStack) to be no
  694. // longer than the specified length, deleting cached DOM elements as necessary.
  695. //
  696. // stack - Array of state IDs
  697. // length - Maximum length to trim to
  698. //
  699. // Returns nothing.
  700. function trimCacheStack(stack, length) {
  701. while (stack.length > length)
  702. delete cacheMapping[stack.shift()]
  703. }
  704. // Public: Find version identifier for the initial page load.
  705. //
  706. // Returns String version or undefined.
  707. function findVersion() {
  708. return $('meta').filter(function() {
  709. var name = $(this).attr('http-equiv');
  710. return name && name.toUpperCase() === 'X-PJAX-VERSION'
  711. }).attr('content')
  712. }
  713. // Install pjax functions on $.pjax to enable pushState behavior.
  714. //
  715. // Does nothing if already enabled.
  716. //
  717. // Examples
  718. //
  719. // $.pjax.enable()
  720. //
  721. // Returns nothing.
  722. function enable() {
  723. $.fn.pjax = fnPjax;
  724. $.pjax = pjax;
  725. $.pjax.enable = $.noop;
  726. $.pjax.disable = disable;
  727. $.pjax.click = handleClick;
  728. $.pjax.submit = handleSubmit;
  729. $.pjax.reload = pjaxReload;
  730. $.pjax.defaults = {
  731. timeout: 650,
  732. push: true,
  733. replace: false,
  734. type: 'GET',
  735. dataType: 'html',
  736. scrollTo: 0,
  737. maxCacheLength: 20,
  738. version: findVersion
  739. };
  740. $(window).on('popstate.pjax', onPjaxPopstate)
  741. }
  742. // Disable pushState behavior.
  743. //
  744. // This is the case when a browser doesn't support pushState. It is
  745. // sometimes useful to disable pushState for debugging on a modern
  746. // browser.
  747. //
  748. // Examples
  749. //
  750. // $.pjax.disable()
  751. //
  752. // Returns nothing.
  753. function disable() {
  754. $.fn.pjax = function() { return this };
  755. $.pjax = fallbackPjax;
  756. $.pjax.enable = enable;
  757. $.pjax.disable = $.noop;
  758. $.pjax.click = $.noop;
  759. $.pjax.submit = $.noop;
  760. $.pjax.reload = function() { window.location.reload() };
  761. $(window).off('popstate.pjax', onPjaxPopstate)
  762. }
  763. // Add the state property to jQuery's event object so we can use it in
  764. // $(window).bind('popstate')
  765. if ($.event.props && $.inArray('state', $.event.props) < 0) {
  766. $.event.props.push('state')
  767. } else if (!('state' in $.Event.prototype)) {
  768. $.event.addProp('state')
  769. }
  770. // Is pjax supported by this browser?
  771. $.support.pjax =
  772. window.history && window.history.pushState && window.history.replaceState &&
  773. // pushState isn't reliable on iOS until 5.
  774. !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/);
  775. if ($.support.pjax) {
  776. enable()
  777. } else {
  778. disable()
  779. }
  780. })(jQuery);