jquery.pjax.js 29 KB

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