pjax.js 26 KB

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