jquery.pjax.js 33 KB

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