[Rt-commit] rt branch, 4.6/pjax, created. rt-4.4.1-3-gadd6803
Shawn Moore
shawn at bestpractical.com
Wed Sep 21 17:09:25 EDT 2016
The branch, 4.6/pjax has been created
at add68035c3c46a67ef65f9448db655869ff9f010 (commit)
- Log -----------------------------------------------------------------
commit f8c7e0d022f9010eee30aaa5065883fa5678a8ff
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Sep 21 18:12:08 2016 +0000
Add jQuery-pjax 1.9.6 library
Source: https://raw.githubusercontent.com/defunkt/jquery-pjax/v1.9.6/jquery.pjax.js
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index e3cf905..1e2d05c 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -115,6 +115,7 @@ sub JSFiles {
jquery.modal.min.js
jquery.modal-defaults.js
jquery.cookie.js
+ jquery.pjax.js
titlebox-state.js
i18n.js
util.js
diff --git a/share/static/js/jquery.pjax.js b/share/static/js/jquery.pjax.js
new file mode 100644
index 0000000..f1728d4
--- /dev/null
+++ b/share/static/js/jquery.pjax.js
@@ -0,0 +1,919 @@
+/*!
+ * 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 - Where to stick the response body. Usually a String selector.
+// $(container).html(xhr.responseBody)
+// (default: current jquery context)
+// 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) {
+ var context = this
+ return this.on('click.pjax', selector, function(event) {
+ var opts = $.extend({}, optionsFor(container, options))
+ if (!opts.container)
+ opts.container = $(this).attr('data-pjax') || context
+ 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')
+//
+// $(document).on('click', 'a', function(event) {
+// var container = $(this).closest('[data-pjax-container]')
+// $.pjax.click(event, container)
+// })
+//
+// Returns nothing.
+function handleClick(event, container, options) {
+ options = optionsFor(container, options)
+
+ var link = event.currentTarget
+
+ 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) {
+// var container = $(this).closest('[data-pjax-container]')
+// $.pjax.submit(event, container)
+// })
+//
+// Returns nothing.
+function handleSubmit(event, container, options) {
+ options = optionsFor(container, options)
+
+ var form = event.currentTarget
+
+ if (form.tagName.toUpperCase() !== 'FORM')
+ throw "$.pjax.submit requires a form element"
+
+ var defaults = {
+ type: form.method.toUpperCase(),
+ url: form.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 - Where to stick the response body.
+// $(container).html(xhr.responseBody)
+// 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 target = options.target
+
+ var hash = parseURL(options.url).hash
+
+ var context = options.context = findContainerFor(options.container)
+
+ // 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: context.selector})
+ } else {
+ options.data._pjax = context.selector
+ }
+
+ function fire(type, args, props) {
+ if (!props) props = {}
+ props.relatedTarget = 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', context.selector)
+
+ 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: context.selector,
+ fragment: options.fragment,
+ timeout: options.timeout
+ }
+
+ if (options.push || options.replace) {
+ window.history.replaceState(pjax.state, container.title, container.url)
+ }
+
+ // Clear out any focused controls before inserting new page contents.
+ try {
+ document.activeElement.blur()
+ } catch (e) { }
+
+ 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: context.selector,
+ 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, 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 container = $(cache[0] || state.container), 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, cloneContents(container))
+ }
+
+ var popstateEvent = $.Event('pjax:popstate', {
+ state: state,
+ direction: direction
+ })
+ container.trigger(popstateEvent)
+
+ var options = {
+ id: state.id,
+ url: state.url,
+ container: container,
+ 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
+ } 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) jQuery._data(this, 'globalEval', false)
+ })
+ return [container.selector, 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, '')
+ 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) {
+ // Both container and options
+ if ( container && options )
+ options.container = container
+
+ // First argument is options Object
+ else if ( $.isPlainObject(container) )
+ options = container
+
+ // Only container
+ else
+ options = {container: container}
+
+ // Find and validate container
+ if (options.container)
+ options.container = findContainerFor(options.container)
+
+ return options
+}
+
+// Internal: Find container element for a variety of inputs.
+//
+// Because we can't persist elements using the history API, we must be
+// able to find a String selector that will consistently find the Element.
+//
+// container - A selector String, jQuery object, or DOM Element.
+//
+// Returns a jQuery object whose context is `document` and has a selector.
+function findContainerFor(container) {
+ container = $(container)
+
+ if ( !container.length ) {
+ throw "no pjax container for " + container.selector
+ } else if ( container.selector !== '' && container.context === document ) {
+ return container
+ } else if ( container.attr('id') ) {
+ return $('#' + container.attr('id'))
+ } else {
+ throw "cant get selector for pjax 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
+
+ // Attempt to parse response html into elements
+ if (fullDocument) {
+ var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))
+ var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
+ } else {
+ var $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) {
+ // If they specified a fragment, look for it in the response
+ // and pull it out.
+ if (options.fragment === 'body') {
+ var $fragment = $body
+ } else {
+ var $fragment = findAll($body, 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)
+ if (id = popStack.pop())
+ 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 ( $.inArray('state', $.event.props) < 0 )
+ $.event.props.push('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)/)
+
+$.support.pjax ? enable() : disable()
+
+})(jQuery);
commit 9b0a299b66ca5dbe2327f04ff513399994e116ed
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Sep 21 19:03:02 2016 +0000
Foundational support for PJAX
While the basic flow does work, our JS and header code are not prepared
to deal with how PJAX changes the request-response model.
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 7722558..90adcc0 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1210,6 +1210,17 @@ Set(@ChartColors, qw(
33cc33 cc3333 cc9933 6633cc
));
+=item C<$EnablePjax>
+
+PJAX "uses ajax and pushState to deliver a fast browsing experience with real
+permalinks, page titles, and a working back button". It improves performance by
+requesting only the HTML body of each page load. This avoids downloading,
+parsing, and executing the JavaScript and CSS on every request.
+
+=cut
+
+Set($EnablePjax, 1);
+
=back
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 1e2d05c..01e2955 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -1414,6 +1414,9 @@ our @GLOBAL_WHITELISTED_ARGS = (
# The NotMobile flag is fine for any page; it's only used to toggle a flag
# in the session related to which interface you get.
'NotMobile',
+
+ # jquery-pjax adds this parameter for PJAX requests
+ '_pjax',
);
our %WHITELISTED_COMPONENT_ARGS = (
@@ -4412,6 +4415,10 @@ sub GetCustomFieldInputNamePrefix {
RT::Interface::Web::GetCustomFieldInputNamePrefix(@_);
}
+sub IsPjaxRequest {
+ RT::Interface::Web::RequestENV('HTTP_X_PJAX');
+}
+
package RT::Interface::Web;
RT::Base->_ImportOverlays();
diff --git a/share/html/Elements/Footer b/share/html/Elements/Footer
index c8c6a5a..77cbe2c 100644
--- a/share/html/Elements/Footer
+++ b/share/html/Elements/Footer
@@ -66,8 +66,10 @@
<%$d->Dump() %>
</pre>
% }
+% unless (IsPjaxRequest()) {
</body>
</html>
+% }
<%ARGS>
$Debug => 0
$Menu => 1
diff --git a/share/html/Elements/Header b/share/html/Elements/Header
index b461da7..d368195 100644
--- a/share/html/Elements/Header
+++ b/share/html/Elements/Header
@@ -45,6 +45,7 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
+% unless (IsPjaxRequest()) {
%# index.html gets two doctypes unless we can skip it here
% unless ($SkipDoctype) {
<!DOCTYPE html>
@@ -97,6 +98,8 @@
<& /Elements/Framekiller &>
</head>
+% }
+
<body class="<% join( ' ',@{$ARGS{'BodyClass'}}) %>" <% $id && qq[id="comp-$id"] |n %>>
% if ($ShowBar) {
diff --git a/share/html/Elements/HeaderJavascript b/share/html/Elements/HeaderJavascript
index 4b5e52b..81b44b0 100644
--- a/share/html/Elements/HeaderJavascript
+++ b/share/html/Elements/HeaderJavascript
@@ -62,6 +62,9 @@ jQuery( loadTitleBoxStates );
% if ( $onload ) {
jQuery( <% $onload |n %> );
% }
+% if ( RT->Config->Get('EnablePjax') ) {
+jQuery(document).pjax('a', 'body', { timeout: 30000 });
+% }
--></script>
<%INIT>
commit add68035c3c46a67ef65f9448db655869ff9f010
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Sep 21 20:12:53 2016 +0000
Implement pjax versioning for forcing full refresh
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 01e2955..a7bbaef 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -1873,6 +1873,23 @@ sub RequestENV {
return $name ? $env->{$name} : $env;
}
+=head2 PjaxVersion
+
+Produces a string which must remain constant for PJAX to continue executing.
+In other words, if this version string changes, then PJAX will force a full
+page reload. Any variables (such as the currently-selected theme) which affect
+assets or layout should be represented here.
+
+=cut
+
+sub PjaxVersion {
+ my $style = $HTML::Mason::Commands::session{'CurrentUser'}
+ ? $HTML::Mason::Commands::session{'CurrentUser'}->Stylesheet
+ : RT->Config->Get('WebDefaultStylesheet');
+
+ return join '-', $RT::VERSION, $style;
+}
+
package HTML::Mason::Commands;
use vars qw/$r $m %session/;
diff --git a/share/html/Elements/Header b/share/html/Elements/Header
index d368195..0ead426 100644
--- a/share/html/Elements/Header
+++ b/share/html/Elements/Header
@@ -62,6 +62,10 @@
<meta http-equiv="refresh" content="<% "$1$URL" %>" />
% }
+% if (RT->Config->Get('EnablePjax')) {
+<meta http-equiv="x-pjax-version" content="<% RT::Interface::Web::PjaxVersion() %>">
+% }
+
<& JavascriptConfig &>
% for my $cssfile ( @css_files ) {
@@ -116,6 +120,10 @@
$r->headers_out->{'Pragma'} = 'no-cache';
$r->headers_out->{'Cache-control'} = 'no-cache';
+if (RT->Config->Get('EnablePjax')) {
+ $r->headers_out->{'X-PJAX-Version'} = RT::Interface::Web::PjaxVersion();
+}
+
my $id = $m->request_comp->path;
$id =~ s|^/||g;
$id =~ s|/|-|g;
-----------------------------------------------------------------------
More information about the rt-commit
mailing list