jquery.pjax.js 28KB

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