|
@@ -0,0 +1,903 @@
|
|
|
|
+/*!
|
|
|
|
+ * Copyright 2012, Chris Wanstrath
|
|
|
|
+ * Released under the MIT License
|
|
|
|
+ * https://github.com/defunkt/jquery-pjax
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+(function($){
|
|
|
|
+
|
|
|
|
+// When called on a container with a selector, fetches the href with
|
|
|
|
+// ajax into the container or with the data-pjax attribute on the link
|
|
|
|
+// itself.
|
|
|
|
+//
|
|
|
|
+// Tries to make sure the back button and ctrl+click work the way
|
|
|
|
+// you'd expect.
|
|
|
|
+//
|
|
|
|
+// Exported as $.fn.pjax
|
|
|
|
+//
|
|
|
|
+// Accepts a jQuery ajax options object that may include these
|
|
|
|
+// pjax specific options:
|
|
|
|
+//
|
|
|
|
+//
|
|
|
|
+// container - String selector for the element where to place the response body.
|
|
|
|
+// push - Whether to pushState the URL. Defaults to true (of course).
|
|
|
|
+// replace - Want to use replaceState instead? That's cool.
|
|
|
|
+//
|
|
|
|
+// For convenience the second parameter can be either the container or
|
|
|
|
+// the options object.
|
|
|
|
+//
|
|
|
|
+// Returns the jQuery object
|
|
|
|
+function fnPjax(selector, container, options) {
|
|
|
|
+ options = optionsFor(container, options)
|
|
|
|
+ return this.on('click.pjax', selector, function(event) {
|
|
|
|
+ var opts = options
|
|
|
|
+ if (!opts.container) {
|
|
|
|
+ opts = $.extend({}, options)
|
|
|
|
+ opts.container = $(this).attr('data-pjax')
|
|
|
|
+ }
|
|
|
|
+ handleClick(event, opts)
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Public: pjax on click handler
|
|
|
|
+//
|
|
|
|
+// Exported as $.pjax.click.
|
|
|
|
+//
|
|
|
|
+// event - "click" jQuery.Event
|
|
|
|
+// options - pjax options
|
|
|
|
+//
|
|
|
|
+// Examples
|
|
|
|
+//
|
|
|
|
+// $(document).on('click', 'a', $.pjax.click)
|
|
|
|
+// // is the same as
|
|
|
|
+// $(document).pjax('a')
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function handleClick(event, container, options) {
|
|
|
|
+ options = optionsFor(container, options)
|
|
|
|
+
|
|
|
|
+ var link = event.currentTarget
|
|
|
|
+ var $link = $(link)
|
|
|
|
+
|
|
|
|
+ if (link.tagName.toUpperCase() !== 'A')
|
|
|
|
+ throw "$.fn.pjax or $.pjax.click requires an anchor element"
|
|
|
|
+
|
|
|
|
+ // Middle click, cmd click, and ctrl click should open
|
|
|
|
+ // links in a new tab as normal.
|
|
|
|
+ if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ // Ignore cross origin links
|
|
|
|
+ if ( location.protocol !== link.protocol || location.hostname !== link.hostname )
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ // Ignore case when a hash is being tacked on the current URL
|
|
|
|
+ if ( link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location) )
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ // Ignore event with default prevented
|
|
|
|
+ if (event.isDefaultPrevented())
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ var defaults = {
|
|
|
|
+ url: link.href,
|
|
|
|
+ container: $link.attr('data-pjax'),
|
|
|
|
+ target: link
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var opts = $.extend({}, defaults, options)
|
|
|
|
+ var clickEvent = $.Event('pjax:click')
|
|
|
|
+ $link.trigger(clickEvent, [opts])
|
|
|
|
+
|
|
|
|
+ if (!clickEvent.isDefaultPrevented()) {
|
|
|
|
+ pjax(opts)
|
|
|
|
+ event.preventDefault()
|
|
|
|
+ $link.trigger('pjax:clicked', [opts])
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Public: pjax on form submit handler
|
|
|
|
+//
|
|
|
|
+// Exported as $.pjax.submit
|
|
|
|
+//
|
|
|
|
+// event - "click" jQuery.Event
|
|
|
|
+// options - pjax options
|
|
|
|
+//
|
|
|
|
+// Examples
|
|
|
|
+//
|
|
|
|
+// $(document).on('submit', 'form', function(event) {
|
|
|
|
+// $.pjax.submit(event, '[data-pjax-container]')
|
|
|
|
+// })
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function handleSubmit(event, container, options) {
|
|
|
|
+ options = optionsFor(container, options)
|
|
|
|
+
|
|
|
|
+ var form = event.currentTarget
|
|
|
|
+ var $form = $(form)
|
|
|
|
+
|
|
|
|
+ if (form.tagName.toUpperCase() !== 'FORM')
|
|
|
|
+ throw "$.pjax.submit requires a form element"
|
|
|
|
+
|
|
|
|
+ var defaults = {
|
|
|
|
+ type: ($form.attr('method') || 'GET').toUpperCase(),
|
|
|
|
+ url: $form.attr('action'),
|
|
|
|
+ container: $form.attr('data-pjax'),
|
|
|
|
+ target: form
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (defaults.type !== 'GET' && window.FormData !== undefined) {
|
|
|
|
+ defaults.data = new FormData(form)
|
|
|
|
+ defaults.processData = false
|
|
|
|
+ defaults.contentType = false
|
|
|
|
+ } else {
|
|
|
|
+ // Can't handle file uploads, exit
|
|
|
|
+ if ($form.find(':file').length) {
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Fallback to manually serializing the fields
|
|
|
|
+ defaults.data = $form.serializeArray()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pjax($.extend({}, defaults, options))
|
|
|
|
+
|
|
|
|
+ event.preventDefault()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Loads a URL with ajax, puts the response body inside a container,
|
|
|
|
+// then pushState()'s the loaded URL.
|
|
|
|
+//
|
|
|
|
+// Works just like $.ajax in that it accepts a jQuery ajax
|
|
|
|
+// settings object (with keys like url, type, data, etc).
|
|
|
|
+//
|
|
|
|
+// Accepts these extra keys:
|
|
|
|
+//
|
|
|
|
+// container - String selector for where to stick the response body.
|
|
|
|
+// push - Whether to pushState the URL. Defaults to true (of course).
|
|
|
|
+// replace - Want to use replaceState instead? That's cool.
|
|
|
|
+//
|
|
|
|
+// Use it just like $.ajax:
|
|
|
|
+//
|
|
|
|
+// var xhr = $.pjax({ url: this.href, container: '#main' })
|
|
|
|
+// console.log( xhr.readyState )
|
|
|
|
+//
|
|
|
|
+// Returns whatever $.ajax returns.
|
|
|
|
+function pjax(options) {
|
|
|
|
+ options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)
|
|
|
|
+
|
|
|
|
+ if ($.isFunction(options.url)) {
|
|
|
|
+ options.url = options.url()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var hash = parseURL(options.url).hash
|
|
|
|
+
|
|
|
|
+ var containerType = $.type(options.container)
|
|
|
|
+ if (containerType !== 'string') {
|
|
|
|
+ throw "expected string value for 'container' option; got " + containerType
|
|
|
|
+ }
|
|
|
|
+ var context = options.context = $(options.container)
|
|
|
|
+ if (!context.length) {
|
|
|
|
+ throw "the container selector '" + options.container + "' did not match anything"
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // We want the browser to maintain two separate internal caches: one
|
|
|
|
+ // for pjax'd partial page loads and one for normal page loads.
|
|
|
|
+ // Without adding this secret parameter, some browsers will often
|
|
|
|
+ // confuse the two.
|
|
|
|
+ if (!options.data) options.data = {}
|
|
|
|
+ if ($.isArray(options.data)) {
|
|
|
|
+ options.data.push({name: '_pjax', value: options.container})
|
|
|
|
+ } else {
|
|
|
|
+ options.data._pjax = options.container
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function fire(type, args, props) {
|
|
|
|
+ if (!props) props = {}
|
|
|
|
+ props.relatedTarget = options.target
|
|
|
|
+ var event = $.Event(type, props)
|
|
|
|
+ context.trigger(event, args)
|
|
|
|
+ return !event.isDefaultPrevented()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var timeoutTimer
|
|
|
|
+
|
|
|
|
+ options.beforeSend = function(xhr, settings) {
|
|
|
|
+ // No timeout for non-GET requests
|
|
|
|
+ // Its not safe to request the resource again with a fallback method.
|
|
|
|
+ if (settings.type !== 'GET') {
|
|
|
|
+ settings.timeout = 0
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ xhr.setRequestHeader('X-PJAX', 'true')
|
|
|
|
+ xhr.setRequestHeader('X-PJAX-Container', options.container)
|
|
|
|
+
|
|
|
|
+ if (!fire('pjax:beforeSend', [xhr, settings]))
|
|
|
|
+ return false
|
|
|
|
+
|
|
|
|
+ if (settings.timeout > 0) {
|
|
|
|
+ timeoutTimer = setTimeout(function() {
|
|
|
|
+ if (fire('pjax:timeout', [xhr, options]))
|
|
|
|
+ xhr.abort('timeout')
|
|
|
|
+ }, settings.timeout)
|
|
|
|
+
|
|
|
|
+ // Clear timeout setting so jquerys internal timeout isn't invoked
|
|
|
|
+ settings.timeout = 0
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var url = parseURL(settings.url)
|
|
|
|
+ if (hash) url.hash = hash
|
|
|
|
+ options.requestUrl = stripInternalParams(url)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ options.complete = function(xhr, textStatus) {
|
|
|
|
+ if (timeoutTimer)
|
|
|
|
+ clearTimeout(timeoutTimer)
|
|
|
|
+
|
|
|
|
+ fire('pjax:complete', [xhr, textStatus, options])
|
|
|
|
+
|
|
|
|
+ fire('pjax:end', [xhr, options])
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ options.error = function(xhr, textStatus, errorThrown) {
|
|
|
|
+ var container = extractContainer("", xhr, options)
|
|
|
|
+
|
|
|
|
+ var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])
|
|
|
|
+ if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
|
|
|
|
+ locationReplace(container.url)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ options.success = function(data, status, xhr) {
|
|
|
|
+ var previousState = pjax.state
|
|
|
|
+
|
|
|
|
+ // If $.pjax.defaults.version is a function, invoke it first.
|
|
|
|
+ // Otherwise it can be a static string.
|
|
|
|
+ var currentVersion = typeof $.pjax.defaults.version === 'function' ?
|
|
|
|
+ $.pjax.defaults.version() :
|
|
|
|
+ $.pjax.defaults.version
|
|
|
|
+
|
|
|
|
+ var latestVersion = xhr.getResponseHeader('X-PJAX-Version')
|
|
|
|
+
|
|
|
|
+ var container = extractContainer(data, xhr, options)
|
|
|
|
+
|
|
|
|
+ var url = parseURL(container.url)
|
|
|
|
+ if (hash) {
|
|
|
|
+ url.hash = hash
|
|
|
|
+ container.url = url.href
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // If there is a layout version mismatch, hard load the new url
|
|
|
|
+ if (currentVersion && latestVersion && currentVersion !== latestVersion) {
|
|
|
|
+ locationReplace(container.url)
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // If the new response is missing a body, hard load the page
|
|
|
|
+ if (!container.contents) {
|
|
|
|
+ locationReplace(container.url)
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pjax.state = {
|
|
|
|
+ id: options.id || uniqueId(),
|
|
|
|
+ url: container.url,
|
|
|
|
+ title: container.title,
|
|
|
|
+ container: options.container,
|
|
|
|
+ fragment: options.fragment,
|
|
|
|
+ timeout: options.timeout
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (options.push || options.replace) {
|
|
|
|
+ window.history.replaceState(pjax.state, container.title, container.url)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Only blur the focus if the focused element is within the container.
|
|
|
|
+ var blurFocus = $.contains(context, document.activeElement)
|
|
|
|
+
|
|
|
|
+ // Clear out any focused controls before inserting new page contents.
|
|
|
|
+ if (blurFocus) {
|
|
|
|
+ try {
|
|
|
|
+ document.activeElement.blur()
|
|
|
|
+ } catch (e) { /* ignore */ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (container.title) document.title = container.title
|
|
|
|
+
|
|
|
|
+ fire('pjax:beforeReplace', [container.contents, options], {
|
|
|
|
+ state: pjax.state,
|
|
|
|
+ previousState: previousState
|
|
|
|
+ })
|
|
|
|
+ context.html(container.contents)
|
|
|
|
+
|
|
|
|
+ // FF bug: Won't autofocus fields that are inserted via JS.
|
|
|
|
+ // This behavior is incorrect. So if theres no current focus, autofocus
|
|
|
|
+ // the last field.
|
|
|
|
+ //
|
|
|
|
+ // http://www.w3.org/html/wg/drafts/html/master/forms.html
|
|
|
|
+ var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0]
|
|
|
|
+ if (autofocusEl && document.activeElement !== autofocusEl) {
|
|
|
|
+ autofocusEl.focus()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ executeScriptTags(container.scripts)
|
|
|
|
+
|
|
|
|
+ var scrollTo = options.scrollTo
|
|
|
|
+
|
|
|
|
+ // Ensure browser scrolls to the element referenced by the URL anchor
|
|
|
|
+ if (hash) {
|
|
|
|
+ var name = decodeURIComponent(hash.slice(1))
|
|
|
|
+ var target = document.getElementById(name) || document.getElementsByName(name)[0]
|
|
|
|
+ if (target) scrollTo = $(target).offset().top
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (typeof scrollTo == 'number') $(window).scrollTop(scrollTo)
|
|
|
|
+
|
|
|
|
+ fire('pjax:success', [data, status, xhr, options])
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ // Initialize pjax.state for the initial page load. Assume we're
|
|
|
|
+ // using the container and options of the link we're loading for the
|
|
|
|
+ // back button to the initial page. This ensures good back button
|
|
|
|
+ // behavior.
|
|
|
|
+ if (!pjax.state) {
|
|
|
|
+ pjax.state = {
|
|
|
|
+ id: uniqueId(),
|
|
|
|
+ url: window.location.href,
|
|
|
|
+ title: document.title,
|
|
|
|
+ container: options.container,
|
|
|
|
+ fragment: options.fragment,
|
|
|
|
+ timeout: options.timeout
|
|
|
|
+ }
|
|
|
|
+ window.history.replaceState(pjax.state, document.title)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Cancel the current request if we're already pjaxing
|
|
|
|
+ abortXHR(pjax.xhr)
|
|
|
|
+
|
|
|
|
+ pjax.options = options
|
|
|
|
+ var xhr = pjax.xhr = $.ajax(options)
|
|
|
|
+
|
|
|
|
+ if (xhr.readyState > 0) {
|
|
|
|
+ if (options.push && !options.replace) {
|
|
|
|
+ // Cache current container element before replacing it
|
|
|
|
+ cachePush(pjax.state.id, [options.container, cloneContents(context)])
|
|
|
|
+
|
|
|
|
+ window.history.pushState(null, "", options.requestUrl)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ fire('pjax:start', [xhr, options])
|
|
|
|
+ fire('pjax:send', [xhr, options])
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return pjax.xhr
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Public: Reload current page with pjax.
|
|
|
|
+//
|
|
|
|
+// Returns whatever $.pjax returns.
|
|
|
|
+function pjaxReload(container, options) {
|
|
|
|
+ var defaults = {
|
|
|
|
+ url: window.location.href,
|
|
|
|
+ push: false,
|
|
|
|
+ replace: true,
|
|
|
|
+ scrollTo: false
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return pjax($.extend(defaults, optionsFor(container, options)))
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Hard replace current state with url.
|
|
|
|
+//
|
|
|
|
+// Work for around WebKit
|
|
|
|
+// https://bugs.webkit.org/show_bug.cgi?id=93506
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function locationReplace(url) {
|
|
|
|
+ window.history.replaceState(null, "", pjax.state.url)
|
|
|
|
+ window.location.replace(url)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+var initialPop = true
|
|
|
|
+var initialURL = window.location.href
|
|
|
|
+var initialState = window.history.state
|
|
|
|
+
|
|
|
|
+// Initialize $.pjax.state if possible
|
|
|
|
+// Happens when reloading a page and coming forward from a different
|
|
|
|
+// session history.
|
|
|
|
+if (initialState && initialState.container) {
|
|
|
|
+ pjax.state = initialState
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Non-webkit browsers don't fire an initial popstate event
|
|
|
|
+if ('state' in window.history) {
|
|
|
|
+ initialPop = false
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// popstate handler takes care of the back and forward buttons
|
|
|
|
+//
|
|
|
|
+// You probably shouldn't use pjax on pages with other pushState
|
|
|
|
+// stuff yet.
|
|
|
|
+function onPjaxPopstate(event) {
|
|
|
|
+
|
|
|
|
+ // Hitting back or forward should override any pending PJAX request.
|
|
|
|
+ if (!initialPop) {
|
|
|
|
+ abortXHR(pjax.xhr)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var previousState = pjax.state
|
|
|
|
+ var state = event.state
|
|
|
|
+ var direction
|
|
|
|
+
|
|
|
|
+ if (state && state.container) {
|
|
|
|
+ // When coming forward from a separate history session, will get an
|
|
|
|
+ // initial pop with a state we are already at. Skip reloading the current
|
|
|
|
+ // page.
|
|
|
|
+ if (initialPop && initialURL == state.url) return
|
|
|
|
+
|
|
|
|
+ if (previousState) {
|
|
|
|
+ // If popping back to the same state, just skip.
|
|
|
|
+ // Could be clicking back from hashchange rather than a pushState.
|
|
|
|
+ if (previousState.id === state.id) return
|
|
|
|
+
|
|
|
|
+ // Since state IDs always increase, we can deduce the navigation direction
|
|
|
|
+ direction = previousState.id < state.id ? 'forward' : 'back'
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var cache = cacheMapping[state.id] || []
|
|
|
|
+ var containerSelector = cache[0] || state.container
|
|
|
|
+ var container = $(containerSelector), contents = cache[1]
|
|
|
|
+
|
|
|
|
+ if (container.length) {
|
|
|
|
+ if (previousState) {
|
|
|
|
+ // Cache current container before replacement and inform the
|
|
|
|
+ // cache which direction the history shifted.
|
|
|
|
+ cachePop(direction, previousState.id, [containerSelector, cloneContents(container)])
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var popstateEvent = $.Event('pjax:popstate', {
|
|
|
|
+ state: state,
|
|
|
|
+ direction: direction
|
|
|
|
+ })
|
|
|
|
+ container.trigger(popstateEvent)
|
|
|
|
+
|
|
|
|
+ var options = {
|
|
|
|
+ id: state.id,
|
|
|
|
+ url: state.url,
|
|
|
|
+ container: containerSelector,
|
|
|
|
+ push: false,
|
|
|
|
+ fragment: state.fragment,
|
|
|
|
+ timeout: state.timeout,
|
|
|
|
+ scrollTo: false
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (contents) {
|
|
|
|
+ container.trigger('pjax:start', [null, options])
|
|
|
|
+
|
|
|
|
+ pjax.state = state
|
|
|
|
+ if (state.title) document.title = state.title
|
|
|
|
+ var beforeReplaceEvent = $.Event('pjax:beforeReplace', {
|
|
|
|
+ state: state,
|
|
|
|
+ previousState: previousState
|
|
|
|
+ })
|
|
|
|
+ container.trigger(beforeReplaceEvent, [contents, options])
|
|
|
|
+ container.html(contents)
|
|
|
|
+
|
|
|
|
+ container.trigger('pjax:end', [null, options])
|
|
|
|
+ } else {
|
|
|
|
+ pjax(options)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Force reflow/relayout before the browser tries to restore the
|
|
|
|
+ // scroll position.
|
|
|
|
+ container[0].offsetHeight // eslint-disable-line no-unused-expressions
|
|
|
|
+ } else {
|
|
|
|
+ locationReplace(location.href)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ initialPop = false
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Fallback version of main pjax function for browsers that don't
|
|
|
|
+// support pushState.
|
|
|
|
+//
|
|
|
|
+// Returns nothing since it retriggers a hard form submission.
|
|
|
|
+function fallbackPjax(options) {
|
|
|
|
+ var url = $.isFunction(options.url) ? options.url() : options.url,
|
|
|
|
+ method = options.type ? options.type.toUpperCase() : 'GET'
|
|
|
|
+
|
|
|
|
+ var form = $('<form>', {
|
|
|
|
+ method: method === 'GET' ? 'GET' : 'POST',
|
|
|
|
+ action: url,
|
|
|
|
+ style: 'display:none'
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ if (method !== 'GET' && method !== 'POST') {
|
|
|
|
+ form.append($('<input>', {
|
|
|
|
+ type: 'hidden',
|
|
|
|
+ name: '_method',
|
|
|
|
+ value: method.toLowerCase()
|
|
|
|
+ }))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var data = options.data
|
|
|
|
+ if (typeof data === 'string') {
|
|
|
|
+ $.each(data.split('&'), function(index, value) {
|
|
|
|
+ var pair = value.split('=')
|
|
|
|
+ form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
|
|
|
|
+ })
|
|
|
|
+ } else if ($.isArray(data)) {
|
|
|
|
+ $.each(data, function(index, value) {
|
|
|
|
+ form.append($('<input>', {type: 'hidden', name: value.name, value: value.value}))
|
|
|
|
+ })
|
|
|
|
+ } else if (typeof data === 'object') {
|
|
|
|
+ var key
|
|
|
|
+ for (key in data)
|
|
|
|
+ form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ $(document.body).append(form)
|
|
|
|
+ form.submit()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Abort an XmlHttpRequest if it hasn't been completed,
|
|
|
|
+// also removing its event handlers.
|
|
|
|
+function abortXHR(xhr) {
|
|
|
|
+ if ( xhr && xhr.readyState < 4) {
|
|
|
|
+ xhr.onreadystatechange = $.noop
|
|
|
|
+ xhr.abort()
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Generate unique id for state object.
|
|
|
|
+//
|
|
|
|
+// Use a timestamp instead of a counter since ids should still be
|
|
|
|
+// unique across page loads.
|
|
|
|
+//
|
|
|
|
+// Returns Number.
|
|
|
|
+function uniqueId() {
|
|
|
|
+ return (new Date).getTime()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function cloneContents(container) {
|
|
|
|
+ var cloned = container.clone()
|
|
|
|
+ // Unmark script tags as already being eval'd so they can get executed again
|
|
|
|
+ // when restored from cache. HAXX: Uses jQuery internal method.
|
|
|
|
+ cloned.find('script').each(function(){
|
|
|
|
+ if (!this.src) $._data(this, 'globalEval', false)
|
|
|
|
+ })
|
|
|
|
+ return cloned.contents()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Strip internal query params from parsed URL.
|
|
|
|
+//
|
|
|
|
+// Returns sanitized url.href String.
|
|
|
|
+function stripInternalParams(url) {
|
|
|
|
+ url.search = url.search.replace(/([?&])(_pjax|_)=[^&]*/g, '').replace(/^&/, '')
|
|
|
|
+ return url.href.replace(/\?($|#)/, '$1')
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Parse URL components and returns a Locationish object.
|
|
|
|
+//
|
|
|
|
+// url - String URL
|
|
|
|
+//
|
|
|
|
+// Returns HTMLAnchorElement that acts like Location.
|
|
|
|
+function parseURL(url) {
|
|
|
|
+ var a = document.createElement('a')
|
|
|
|
+ a.href = url
|
|
|
|
+ return a
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Return the `href` component of given URL object with the hash
|
|
|
|
+// portion removed.
|
|
|
|
+//
|
|
|
|
+// location - Location or HTMLAnchorElement
|
|
|
|
+//
|
|
|
|
+// Returns String
|
|
|
|
+function stripHash(location) {
|
|
|
|
+ return location.href.replace(/#.*/, '')
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Build options Object for arguments.
|
|
|
|
+//
|
|
|
|
+// For convenience the first parameter can be either the container or
|
|
|
|
+// the options object.
|
|
|
|
+//
|
|
|
|
+// Examples
|
|
|
|
+//
|
|
|
|
+// optionsFor('#container')
|
|
|
|
+// // => {container: '#container'}
|
|
|
|
+//
|
|
|
|
+// optionsFor('#container', {push: true})
|
|
|
|
+// // => {container: '#container', push: true}
|
|
|
|
+//
|
|
|
|
+// optionsFor({container: '#container', push: true})
|
|
|
|
+// // => {container: '#container', push: true}
|
|
|
|
+//
|
|
|
|
+// Returns options Object.
|
|
|
|
+function optionsFor(container, options) {
|
|
|
|
+ if (container && options) {
|
|
|
|
+ options = $.extend({}, options)
|
|
|
|
+ options.container = container
|
|
|
|
+ return options
|
|
|
|
+ } else if ($.isPlainObject(container)) {
|
|
|
|
+ return container
|
|
|
|
+ } else {
|
|
|
|
+ return {container: container}
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Filter and find all elements matching the selector.
|
|
|
|
+//
|
|
|
|
+// Where $.fn.find only matches descendants, findAll will test all the
|
|
|
|
+// top level elements in the jQuery object as well.
|
|
|
|
+//
|
|
|
|
+// elems - jQuery object of Elements
|
|
|
|
+// selector - String selector to match
|
|
|
|
+//
|
|
|
|
+// Returns a jQuery object.
|
|
|
|
+function findAll(elems, selector) {
|
|
|
|
+ return elems.filter(selector).add(elems.find(selector))
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function parseHTML(html) {
|
|
|
|
+ return $.parseHTML(html, document, true)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: Extracts container and metadata from response.
|
|
|
|
+//
|
|
|
|
+// 1. Extracts X-PJAX-URL header if set
|
|
|
|
+// 2. Extracts inline <title> tags
|
|
|
|
+// 3. Builds response Element and extracts fragment if set
|
|
|
|
+//
|
|
|
|
+// data - String response data
|
|
|
|
+// xhr - XHR response
|
|
|
|
+// options - pjax options Object
|
|
|
|
+//
|
|
|
|
+// Returns an Object with url, title, and contents keys.
|
|
|
|
+function extractContainer(data, xhr, options) {
|
|
|
|
+ var obj = {}, fullDocument = /<html/i.test(data)
|
|
|
|
+
|
|
|
|
+ // Prefer X-PJAX-URL header if it was set, otherwise fallback to
|
|
|
|
+ // using the original requested url.
|
|
|
|
+ var serverUrl = xhr.getResponseHeader('X-PJAX-URL')
|
|
|
|
+ obj.url = serverUrl ? stripInternalParams(parseURL(serverUrl)) : options.requestUrl
|
|
|
|
+
|
|
|
|
+ var $head, $body
|
|
|
|
+ // Attempt to parse response html into elements
|
|
|
|
+ if (fullDocument) {
|
|
|
|
+ $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
|
|
|
|
+ var head = data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)
|
|
|
|
+ $head = head != null ? $(parseHTML(head[0])) : $body
|
|
|
|
+ } else {
|
|
|
|
+ $head = $body = $(parseHTML(data))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // If response data is empty, return fast
|
|
|
|
+ if ($body.length === 0)
|
|
|
|
+ return obj
|
|
|
|
+
|
|
|
|
+ // If there's a <title> tag in the header, use it as
|
|
|
|
+ // the page's title.
|
|
|
|
+ obj.title = findAll($head, 'title').last().text()
|
|
|
|
+
|
|
|
|
+ if (options.fragment) {
|
|
|
|
+ var $fragment = $body
|
|
|
|
+ // If they specified a fragment, look for it in the response
|
|
|
|
+ // and pull it out.
|
|
|
|
+ if (options.fragment !== 'body') {
|
|
|
|
+ $fragment = findAll($fragment, options.fragment).first()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if ($fragment.length) {
|
|
|
|
+ obj.contents = options.fragment === 'body' ? $fragment : $fragment.contents()
|
|
|
|
+
|
|
|
|
+ // If there's no title, look for data-title and title attributes
|
|
|
|
+ // on the fragment
|
|
|
|
+ if (!obj.title)
|
|
|
|
+ obj.title = $fragment.attr('title') || $fragment.data('title')
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ } else if (!fullDocument) {
|
|
|
|
+ obj.contents = $body
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Clean up any <title> tags
|
|
|
|
+ if (obj.contents) {
|
|
|
|
+ // Remove any parent title elements
|
|
|
|
+ obj.contents = obj.contents.not(function() { return $(this).is('title') })
|
|
|
|
+
|
|
|
|
+ // Then scrub any titles from their descendants
|
|
|
|
+ obj.contents.find('title').remove()
|
|
|
|
+
|
|
|
|
+ // Gather all script[src] elements
|
|
|
|
+ obj.scripts = findAll(obj.contents, 'script[src]').remove()
|
|
|
|
+ obj.contents = obj.contents.not(obj.scripts)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Trim any whitespace off the title
|
|
|
|
+ if (obj.title) obj.title = $.trim(obj.title)
|
|
|
|
+
|
|
|
|
+ return obj
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Load an execute scripts using standard script request.
|
|
|
|
+//
|
|
|
|
+// Avoids jQuery's traditional $.getScript which does a XHR request and
|
|
|
|
+// globalEval.
|
|
|
|
+//
|
|
|
|
+// scripts - jQuery object of script Elements
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function executeScriptTags(scripts) {
|
|
|
|
+ if (!scripts) return
|
|
|
|
+
|
|
|
|
+ var existingScripts = $('script[src]')
|
|
|
|
+
|
|
|
|
+ scripts.each(function() {
|
|
|
|
+ var src = this.src
|
|
|
|
+ var matchedScripts = existingScripts.filter(function() {
|
|
|
|
+ return this.src === src
|
|
|
|
+ })
|
|
|
|
+ if (matchedScripts.length) return
|
|
|
|
+
|
|
|
|
+ var script = document.createElement('script')
|
|
|
|
+ var type = $(this).attr('type')
|
|
|
|
+ if (type) script.type = type
|
|
|
|
+ script.src = $(this).attr('src')
|
|
|
|
+ document.head.appendChild(script)
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Internal: History DOM caching class.
|
|
|
|
+var cacheMapping = {}
|
|
|
|
+var cacheForwardStack = []
|
|
|
|
+var cacheBackStack = []
|
|
|
|
+
|
|
|
|
+// Push previous state id and container contents into the history
|
|
|
|
+// cache. Should be called in conjunction with `pushState` to save the
|
|
|
|
+// previous container contents.
|
|
|
|
+//
|
|
|
|
+// id - State ID Number
|
|
|
|
+// value - DOM Element to cache
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function cachePush(id, value) {
|
|
|
|
+ cacheMapping[id] = value
|
|
|
|
+ cacheBackStack.push(id)
|
|
|
|
+
|
|
|
|
+ // Remove all entries in forward history stack after pushing a new page.
|
|
|
|
+ trimCacheStack(cacheForwardStack, 0)
|
|
|
|
+
|
|
|
|
+ // Trim back history stack to max cache length.
|
|
|
|
+ trimCacheStack(cacheBackStack, pjax.defaults.maxCacheLength)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Shifts cache from directional history cache. Should be
|
|
|
|
+// called on `popstate` with the previous state id and container
|
|
|
|
+// contents.
|
|
|
|
+//
|
|
|
|
+// direction - "forward" or "back" String
|
|
|
|
+// id - State ID Number
|
|
|
|
+// value - DOM Element to cache
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function cachePop(direction, id, value) {
|
|
|
|
+ var pushStack, popStack
|
|
|
|
+ cacheMapping[id] = value
|
|
|
|
+
|
|
|
|
+ if (direction === 'forward') {
|
|
|
|
+ pushStack = cacheBackStack
|
|
|
|
+ popStack = cacheForwardStack
|
|
|
|
+ } else {
|
|
|
|
+ pushStack = cacheForwardStack
|
|
|
|
+ popStack = cacheBackStack
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pushStack.push(id)
|
|
|
|
+ id = popStack.pop()
|
|
|
|
+ if (id) delete cacheMapping[id]
|
|
|
|
+
|
|
|
|
+ // Trim whichever stack we just pushed to to max cache length.
|
|
|
|
+ trimCacheStack(pushStack, pjax.defaults.maxCacheLength)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Trim a cache stack (either cacheBackStack or cacheForwardStack) to be no
|
|
|
|
+// longer than the specified length, deleting cached DOM elements as necessary.
|
|
|
|
+//
|
|
|
|
+// stack - Array of state IDs
|
|
|
|
+// length - Maximum length to trim to
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function trimCacheStack(stack, length) {
|
|
|
|
+ while (stack.length > length)
|
|
|
|
+ delete cacheMapping[stack.shift()]
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Public: Find version identifier for the initial page load.
|
|
|
|
+//
|
|
|
|
+// Returns String version or undefined.
|
|
|
|
+function findVersion() {
|
|
|
|
+ return $('meta').filter(function() {
|
|
|
|
+ var name = $(this).attr('http-equiv')
|
|
|
|
+ return name && name.toUpperCase() === 'X-PJAX-VERSION'
|
|
|
|
+ }).attr('content')
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Install pjax functions on $.pjax to enable pushState behavior.
|
|
|
|
+//
|
|
|
|
+// Does nothing if already enabled.
|
|
|
|
+//
|
|
|
|
+// Examples
|
|
|
|
+//
|
|
|
|
+// $.pjax.enable()
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function enable() {
|
|
|
|
+ $.fn.pjax = fnPjax
|
|
|
|
+ $.pjax = pjax
|
|
|
|
+ $.pjax.enable = $.noop
|
|
|
|
+ $.pjax.disable = disable
|
|
|
|
+ $.pjax.click = handleClick
|
|
|
|
+ $.pjax.submit = handleSubmit
|
|
|
|
+ $.pjax.reload = pjaxReload
|
|
|
|
+ $.pjax.defaults = {
|
|
|
|
+ timeout: 650,
|
|
|
|
+ push: true,
|
|
|
|
+ replace: false,
|
|
|
|
+ type: 'GET',
|
|
|
|
+ dataType: 'html',
|
|
|
|
+ scrollTo: 0,
|
|
|
|
+ maxCacheLength: 20,
|
|
|
|
+ version: findVersion
|
|
|
|
+ }
|
|
|
|
+ $(window).on('popstate.pjax', onPjaxPopstate)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Disable pushState behavior.
|
|
|
|
+//
|
|
|
|
+// This is the case when a browser doesn't support pushState. It is
|
|
|
|
+// sometimes useful to disable pushState for debugging on a modern
|
|
|
|
+// browser.
|
|
|
|
+//
|
|
|
|
+// Examples
|
|
|
|
+//
|
|
|
|
+// $.pjax.disable()
|
|
|
|
+//
|
|
|
|
+// Returns nothing.
|
|
|
|
+function disable() {
|
|
|
|
+ $.fn.pjax = function() { return this }
|
|
|
|
+ $.pjax = fallbackPjax
|
|
|
|
+ $.pjax.enable = enable
|
|
|
|
+ $.pjax.disable = $.noop
|
|
|
|
+ $.pjax.click = $.noop
|
|
|
|
+ $.pjax.submit = $.noop
|
|
|
|
+ $.pjax.reload = function() { window.location.reload() }
|
|
|
|
+
|
|
|
|
+ $(window).off('popstate.pjax', onPjaxPopstate)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// Add the state property to jQuery's event object so we can use it in
|
|
|
|
+// $(window).bind('popstate')
|
|
|
|
+if ($.event.props && $.inArray('state', $.event.props) < 0) {
|
|
|
|
+ $.event.props.push('state')
|
|
|
|
+} else if (!('state' in $.Event.prototype)) {
|
|
|
|
+ $.event.addProp('state')
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Is pjax supported by this browser?
|
|
|
|
+$.support.pjax =
|
|
|
|
+ window.history && window.history.pushState && window.history.replaceState &&
|
|
|
|
+ // pushState isn't reliable on iOS until 5.
|
|
|
|
+ !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/)
|
|
|
|
+
|
|
|
|
+if ($.support.pjax) {
|
|
|
|
+ enable()
|
|
|
|
+} else {
|
|
|
|
+ disable()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+})(jQuery)
|