jquery.pjax.js 25 KB

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