You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

924 lines
25KB

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