jquery.pjax.js 25 KB

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