./vendor/assets/javascripts/jquery.pjax.js in rails-pjax-0.0.1 vs ./vendor/assets/javascripts/jquery.pjax.js in rails-pjax-0.0.2

- old
+ new

@@ -22,12 +22,12 @@ // For convenience the first parameter can be either the container or // the options object. // // Returns the jQuery object $.fn.pjax = function( container, options ) { - return this.live('click.pjax', function(event){ - handleClick(event, container, options) + return this.live('click', function(event){ + return handleClick(event, container, options) }) } // Public: pjax on click handler // @@ -51,15 +51,11 @@ function handleClick(event, container, options) { options = optionsFor(container, options) var link = event.currentTarget - // If current target isnt a link, try to find the first A descendant if (link.tagName.toUpperCase() !== 'A') - link = $(link).find('a')[0] - - if (!link) 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 ) @@ -83,13 +79,37 @@ } $.pjax($.extend({}, defaults, options)) event.preventDefault() + return false } +// Internal: Strips _pjax param from url +// +// url - String +// +// Returns String. +function stripPjaxParam(url) { + return url + .replace(/\?_pjax=[^&]+&?/, '?') + .replace(/_pjax=[^&]+&?/, '') + .replace(/[\?&]$/, '') +} +// 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 +} + + // 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). @@ -117,11 +137,12 @@ var target = options.target // DEPRECATED: use options.target if (!target && options.clickedElement) target = options.clickedElement[0] - var hash = parseURL(options.url).hash + var url = options.url + var hash = parseURL(url).hash // DEPRECATED: Save references to original event callbacks. However, // listening for custom pjax:* events is prefered. var oldBeforeSend = options.beforeSend, oldComplete = options.complete, @@ -144,10 +165,12 @@ } var timeoutTimer options.beforeSend = function(xhr, settings) { + url = stripPjaxParam(settings.url) + if (settings.timeout > 0) { timeoutTimer = setTimeout(function() { if (fire('pjax:timeout', [xhr, options])) xhr.abort('timeout') }, settings.timeout) @@ -167,22 +190,13 @@ if (result === false) return false } if (!fire('pjax:beforeSend', [xhr, settings])) return false - if (options.push && !options.replace) { - // Cache current container element before replacing it - containerCache.push(pjax.state.id, context.clone(true, true).contents()) - - window.history.pushState(null, "", options.url) - } - fire('pjax:start', [xhr, options]) // start.pjax is deprecated fire('start.pjax', [xhr, options]) - - fire('pjax:send', [xhr, settings]) } options.complete = function(xhr, textStatus) { if (timeoutTimer) clearTimeout(timeoutTimer) @@ -196,47 +210,77 @@ // end.pjax is deprecated fire('end.pjax', [xhr, options]) } options.error = function(xhr, textStatus, errorThrown) { - var container = extractContainer("", xhr, options) + var respUrl = xhr.getResponseHeader('X-PJAX-URL') + if (respUrl) url = stripPjaxParam(respUrl) // DEPRECATED: Invoke original `error` handler if (oldError) oldError.apply(this, arguments) var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]) if (textStatus !== 'abort' && allowed) - window.location = container.url + window.location = url } options.success = function(data, status, xhr) { - var container = extractContainer(data, xhr, options) + var respUrl = xhr.getResponseHeader('X-PJAX-URL') + if (respUrl) url = stripPjaxParam(respUrl) - if (!container.contents) { - window.location = container.url - return + var title, oldTitle = document.title + + if ( options.fragment ) { + // If they specified a fragment, look for it in the response + // and pull it out. + var html = $('<html>').html(data) + var $fragment = html.find(options.fragment) + if ( $fragment.length ) { + this.html($fragment.contents()) + + // If there's a <title> tag in the response, use it as + // the page's title. Otherwise, look for data-title and title attributes. + title = html.find('title').text() || $fragment.attr('title') || $fragment.data('title') + } else { + return window.location = url + } + } else { + // If we got no data or an entire web page, go directly + // to the page and let normal error handling happen. + if ( !$.trim(data) || /<html/i.test(data) ) + return window.location = url + + this.html(data) + + // If there's a <title> tag in the response, use it as + // the page's title. + title = this.find('title').remove().text() } - pjax.state = { - id: options.id || uniqueId(), - url: container.url, - container: context.selector, + if ( title ) document.title = $.trim(title) + + var state = { + url: url, + pjax: this.selector, fragment: options.fragment, timeout: options.timeout } - if (options.push || options.replace) { - window.history.replaceState(pjax.state, container.title, container.url) + if ( options.replace ) { + pjax.active = true + window.history.replaceState(state, document.title, url) + } else if ( options.push ) { + // this extra replaceState before first push ensures good back + // button behavior + if ( !pjax.active ) { + window.history.replaceState($.extend({}, state, {url:null}), oldTitle) + pjax.active = true + } + + window.history.pushState(state, document.title, url) } - if (container.title) document.title = container.title - context.html(container.contents) - - // Scroll to top by default - if (typeof options.scrollTo === 'number') - $(window).scrollTop(options.scrollTo) - // Google Analytics support if ( (options.replace || options.push) && window._gaq ) _gaq.push(['_trackPageview']) // If the URL has a hash in it, make sure the browser @@ -250,75 +294,25 @@ 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, - 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 var xhr = pjax.xhr if ( xhr && xhr.readyState < 4) { xhr.onreadystatechange = $.noop xhr.abort() } pjax.options = options pjax.xhr = $.ajax(options) - - // pjax event is deprecated $(document).trigger('pjax', [pjax.xhr, options]) return pjax.xhr } -// 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() -} - -// Internal: Strips _pjax param from url -// -// url - String -// -// Returns String. -function stripPjaxParam(url) { - return url - .replace(/\?_pjax=[^&]+&?/, '?') - .replace(/_pjax=[^&]+&?/, '') - .replace(/[\?&]$/, '') -} - -// 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: Build options Object for arguments. // // For convenience the first parameter can be either the container or // the options object. // @@ -374,185 +368,19 @@ } 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) { - var results = $() - elems.each(function() { - if ($(this).is(selector)) - results = results.add(this) - results = results.add(selector, this) - }) - return results -} -// 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 = {} - - // Prefer X-PJAX-URL header if it was set, otherwise fallback to - // using the original requested url. - obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.url) - - // Attempt to parse response html into elements - var $data = $(data) - - // If response data is empty, return fast - if ($data.length === 0) - return obj - - // If there's a <title> tag in the response, use it as - // the page's title. - obj.title = findAll($data, 'title').last().text() - - if (options.fragment) { - // If they specified a fragment, look for it in the response - // and pull it out. - var $fragment = findAll($data, options.fragment).first() - - if ($fragment.length) { - obj.contents = $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 (!/<html/i.test(data)) { - obj.contents = $data - } - - // Clean up any <title> tags - if (obj.contents) { - // Remove any parent title elements - obj.contents = obj.contents.not('title') - - // Then scrub any titles from their descendents - obj.contents.find('title').remove() - } - - // Trim any whitespace off the title - if (obj.title) obj.title = $.trim(obj.title) - - return obj -} - -// Public: Reload current page with pjax. -// -// Returns whatever $.pjax returns. -pjax.reload = function(container, options) { - var defaults = { - url: window.location.href, - push: false, - replace: true, - scrollTo: false - } - - return $.pjax($.extend(defaults, optionsFor(container, options))) -} - - pjax.defaults = { timeout: 650, push: true, replace: false, type: 'GET', - dataType: 'html', - scrollTo: 0, - maxCacheLength: 20 + dataType: 'html' } -// Internal: History DOM caching class. -function Cache() { - this.mapping = {} - this.forwardStack = [] - this.backStack = [] -} -// 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. -Cache.prototype.push = function(id, value) { - this.mapping[id] = value - this.backStack.push(id) - - // Remove all entires in forward history stack after pushing - // a new page. - while (this.forwardStack.length) - delete this.mapping[this.forwardStack.shift()] - - // Trim back history stack to max cache length. - while (this.backStack.length > pjax.defaults.maxCacheLength) - delete this.mapping[this.backStack.shift()] -} -// Retrieve cached DOM Element for state id. -// -// id - State ID Number -// -// Returns DOM Element(s) or undefined if cache miss. -Cache.prototype.get = function(id) { - return this.mapping[id] -} -// Shifts cache from forward history cache to back stack. Should be -// called on `popstate` with the previous state id and container -// contents. -// -// id - State ID Number -// value - DOM Element to cache -// -// Returns nothing. -Cache.prototype.forward = function(id, value) { - this.mapping[id] = value - this.backStack.push(id) - - if (id = this.forwardStack.pop()) - delete this.mapping[id] -} -// Shifts cache from back history cache to forward stack. Should be -// called on `popstate` with the previous state id and container -// contents. -// -// id - State ID Number -// value - DOM Element to cache -// -// Returns nothing. -Cache.prototype.back = function(id, value) { - this.mapping[id] = value - this.forwardStack.push(id) - - if (id = this.backStack.pop()) - delete this.mapping[id] -} - -var containerCache = new Cache - - // Export $.pjax.click pjax.click = handleClick // Used to detect initial (useless) popstate. @@ -570,58 +398,22 @@ popped = true if ( initialPop ) return var state = event.state - if (state && state.container) { - var container = $(state.container) - if (container.length) { - var contents = containerCache.get(state.id) - - if (pjax.state) { - // Since state ids always increase, we can deduce the history - // direction from the previous state. - var direction = pjax.state.id < state.id ? 'forward' : 'back' - - // Cache current container before replacement and inform the - // cache which direction the history shifted. - containerCache[direction](pjax.state.id, container.clone(true, true).contents()) - } - - var options = { - id: state.id, - url: state.url, + if ( state && state.pjax ) { + var container = state.pjax + if ( $(container+'').length ) + $.pjax({ + url: state.url || location.href, + fragment: state.fragment, container: container, push: false, - fragment: state.fragment, - timeout: state.timeout, - scrollTo: false - } - - if (contents) { - // pjax event is deprecated - $(document).trigger('pjax', [null, options]) - container.trigger('pjax:start', [null, options]) - // end.pjax event is deprecated - container.trigger('start.pjax', [null, options]) - - container.html(contents) - pjax.state = state - - container.trigger('pjax:end', [null, options]) - // end.pjax event is deprecated - container.trigger('end.pjax', [null, options]) - } else { - $.pjax(options) - } - - // Force reflow/relayout before the browser tries to restore the - // scroll position. - container[0].offsetHeight - } else { + timeout: state.timeout + }) + else window.location = location.href - } } }) // Add the state property to jQuery's event object so we can use it in @@ -670,10 +462,9 @@ $(document.body).append(form) form.submit() } $.pjax.click = $.noop - $.pjax.reload = window.location.reload $.fn.pjax = function() { return this } } })(jQuery);