|
| 1 | +// jquery.pjax.js |
| 2 | +// copyright chris wanstrath |
| 3 | +// https://github.com/defunkt/jquery-pjax |
| 4 | + |
| 5 | +(function($){ |
| 6 | + |
| 7 | +// When called on a link, fetches the href with ajax into the |
| 8 | +// container specified as the first parameter or with the data-pjax |
| 9 | +// attribute on the link itself. |
| 10 | +// |
| 11 | +// Tries to make sure the back button and ctrl+click work the way |
| 12 | +// you'd expect. |
| 13 | +// |
| 14 | +// Accepts a jQuery ajax options object that may include these |
| 15 | +// pjax specific options: |
| 16 | +// |
| 17 | +// container - Where to stick the response body. Usually a String selector. |
| 18 | +// $(container).html(xhr.responseBody) |
| 19 | +// push - Whether to pushState the URL. Defaults to true (of course). |
| 20 | +// replace - Want to use replaceState instead? That's cool. |
| 21 | +// |
| 22 | +// For convenience the first parameter can be either the container or |
| 23 | +// the options object. |
| 24 | +// |
| 25 | +// Returns the jQuery object |
| 26 | +$.fn.pjax = function( container, options ) { |
| 27 | + if ( options ) |
| 28 | + options.container = container |
| 29 | + else |
| 30 | + options = $.isPlainObject(container) ? container : {container:container} |
| 31 | + |
| 32 | + // We can't persist $objects using the history API so we must use |
| 33 | + // a String selector. Bail if we got anything else. |
| 34 | + if ( options.container && typeof options.container !== 'string' ) { |
| 35 | + throw "pjax container must be a string selector!" |
| 36 | + return false |
| 37 | + } |
| 38 | + |
| 39 | + return this.live('click', function(event){ |
| 40 | + // Middle click, cmd click, and ctrl click should open |
| 41 | + // links in a new tab as normal. |
| 42 | + if ( event.which > 1 || event.metaKey ) |
| 43 | + return true |
| 44 | + |
| 45 | + var defaults = { |
| 46 | + url: (this.href || $(this).attr('data-href')), |
| 47 | + container: $(this).attr('data-pjax'), |
| 48 | + clickedElement: $(this), |
| 49 | + fragment: null |
| 50 | + } |
| 51 | + |
| 52 | + $.pjax($.extend({}, defaults, options)) |
| 53 | + |
| 54 | + event.preventDefault() |
| 55 | + }) |
| 56 | +} |
| 57 | + |
| 58 | + |
| 59 | +// Loads a URL with ajax, puts the response body inside a container, |
| 60 | +// then pushState()'s the loaded URL. |
| 61 | +// |
| 62 | +// Works just like $.ajax in that it accepts a jQuery ajax |
| 63 | +// settings object (with keys like url, type, data, etc). |
| 64 | +// |
| 65 | +// Accepts these extra keys: |
| 66 | +// |
| 67 | +// container - Where to stick the response body. Must be a String. |
| 68 | +// $(container).html(xhr.responseBody) |
| 69 | +// push - Whether to pushState the URL. Defaults to true (of course). |
| 70 | +// replace - Want to use replaceState instead? That's cool. |
| 71 | +// |
| 72 | +// Use it just like $.ajax: |
| 73 | +// |
| 74 | +// var xhr = $.pjax({ url: this.href, container: '#main' }) |
| 75 | +// console.log( xhr.readyState ) |
| 76 | +// |
| 77 | +// Returns whatever $.ajax returns. |
| 78 | +var pjax = $.pjax = function( options ) { |
| 79 | + var $container = $(options.container), |
| 80 | + success = options.success || $.noop |
| 81 | + |
| 82 | + // We don't want to let anyone override our success handler. |
| 83 | + delete options.success |
| 84 | + |
| 85 | + // We can't persist $objects using the history API so we must use |
| 86 | + // a String selector. Bail if we got anything else. |
| 87 | + if ( typeof options.container !== 'string' ) |
| 88 | + throw "pjax container must be a string selector!" |
| 89 | + |
| 90 | + options = $.extend(true, {}, pjax.defaults, options) |
| 91 | + |
| 92 | + if ( $.isFunction(options.url) ) { |
| 93 | + options.url = options.url() |
| 94 | + } |
| 95 | + |
| 96 | + options.context = $container |
| 97 | + options.success = function(data){ |
| 98 | + if ( options.fragment ) { |
| 99 | + // If they specified a fragment, look for it in the response |
| 100 | + // and pull it out. |
| 101 | + var $fragment = $(data).find(options.fragment) |
| 102 | + if ( $fragment.length ) |
| 103 | + data = $fragment.children() |
| 104 | + else |
| 105 | + return window.location = options.url |
| 106 | + } else { |
| 107 | + // If we got no data or an entire web page, go directly |
| 108 | + // to the page and let normal error handling happen. |
| 109 | + if ( !$.trim(data) || /<html/i.test(data) ) |
| 110 | + return window.location = options.url |
| 111 | + } |
| 112 | + |
| 113 | + // Make it happen. |
| 114 | + this.html(data) |
| 115 | + |
| 116 | + // If there's a <title> tag in the response, use it as |
| 117 | + // the page's title. |
| 118 | + var oldTitle = document.title, |
| 119 | + title = $.trim( this.find('title').remove().text() ) |
| 120 | + if ( title ) document.title = title |
| 121 | + |
| 122 | + // No <title>? Fragment? Look for data-title and title attributes. |
| 123 | + if ( !title && options.fragment ) { |
| 124 | + title = $fragment.attr('title') || $fragment.data('title') |
| 125 | + } |
| 126 | + |
| 127 | + var state = { |
| 128 | + pjax: options.container, |
| 129 | + fragment: options.fragment, |
| 130 | + timeout: options.timeout |
| 131 | + } |
| 132 | + |
| 133 | + // If there are extra params, save the complete URL in the state object |
| 134 | + var query = $.param(options.data) |
| 135 | + if ( query != "_pjax=true" ) |
| 136 | + state.url = options.url + (/\?/.test(options.url) ? "&" : "?") + query |
| 137 | + |
| 138 | + if ( options.replace ) { |
| 139 | + window.history.replaceState(state, document.title, options.url) |
| 140 | + } else if ( options.push ) { |
| 141 | + // this extra replaceState before first push ensures good back |
| 142 | + // button behavior |
| 143 | + if ( !pjax.active ) { |
| 144 | + window.history.replaceState($.extend({}, state, {url:null}), oldTitle) |
| 145 | + pjax.active = true |
| 146 | + } |
| 147 | + |
| 148 | + window.history.pushState(state, document.title, options.url) |
| 149 | + } |
| 150 | + |
| 151 | + // Google Analytics support |
| 152 | + if ( (options.replace || options.push) && window._gaq ) |
| 153 | + _gaq.push(['_trackPageview']) |
| 154 | + |
| 155 | + // If the URL has a hash in it, make sure the browser |
| 156 | + // knows to navigate to the hash. |
| 157 | + var hash = window.location.hash.toString() |
| 158 | + if ( hash !== '' ) { |
| 159 | + window.location.href = hash |
| 160 | + } |
| 161 | + |
| 162 | + // Invoke their success handler if they gave us one. |
| 163 | + success.apply(this, arguments) |
| 164 | + } |
| 165 | + |
| 166 | + // Cancel the current request if we're already pjaxing |
| 167 | + var xhr = pjax.xhr |
| 168 | + if ( xhr && xhr.readyState < 4) { |
| 169 | + xhr.onreadystatechange = $.noop |
| 170 | + xhr.abort() |
| 171 | + } |
| 172 | + |
| 173 | + pjax.options = options |
| 174 | + pjax.xhr = $.ajax(options) |
| 175 | + $(document).trigger('pjax', [pjax.xhr, options]) |
| 176 | + |
| 177 | + return pjax.xhr |
| 178 | +} |
| 179 | + |
| 180 | + |
| 181 | +pjax.defaults = { |
| 182 | + timeout: 2000, |
| 183 | + push: true, |
| 184 | + replace: false, |
| 185 | + // We want the browser to maintain two separate internal caches: one for |
| 186 | + // pjax'd partial page loads and one for normal page loads. Without |
| 187 | + // adding this secret parameter, some browsers will often confuse the two. |
| 188 | + data: { _pjax: true }, |
| 189 | + type: 'GET', |
| 190 | + dataType: 'html', |
| 191 | + beforeSend: function(xhr){ |
| 192 | + this.trigger('pjax:start', [xhr, pjax.options]) |
| 193 | + // start.pjax is deprecated |
| 194 | + this.trigger('start.pjax', [xhr, pjax.options]) |
| 195 | + xhr.setRequestHeader('X-PJAX', 'true') |
| 196 | + }, |
| 197 | + error: function(xhr, textStatus, errorThrown){ |
| 198 | + if ( textStatus !== 'abort' ) { |
| 199 | + // console.log(xhr, textStatus, errorThrown) |
| 200 | + window.location = pjax.options.url |
| 201 | + } |
| 202 | + |
| 203 | + }, |
| 204 | + complete: function(xhr){ |
| 205 | + this.trigger('pjax:end', [xhr, pjax.options]) |
| 206 | + // end.pjax is deprecated |
| 207 | + this.trigger('end.pjax', [xhr, pjax.options]) |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | + |
| 212 | +// Used to detect initial (useless) popstate. |
| 213 | +// If history.state exists, assume browser isn't going to fire initial popstate. |
| 214 | +var popped = ('state' in window.history), initialURL = location.href |
| 215 | + |
| 216 | + |
| 217 | +// popstate handler takes care of the back and forward buttons |
| 218 | +// |
| 219 | +// You probably shouldn't use pjax on pages with other pushState |
| 220 | +// stuff yet. |
| 221 | +$(window).bind('popstate', function(event){ |
| 222 | + // Ignore inital popstate that some browsers fire on page load |
| 223 | + var initialPop = !popped && location.href == initialURL |
| 224 | + popped = true |
| 225 | + if ( initialPop ) return |
| 226 | + |
| 227 | + var state = event.state |
| 228 | + |
| 229 | + if ( state && state.pjax ) { |
| 230 | + var container = state.pjax |
| 231 | + if ( $(container+'').length ) |
| 232 | + $.pjax({ |
| 233 | + url: state.url || location.href, |
| 234 | + fragment: state.fragment, |
| 235 | + container: container, |
| 236 | + push: false, |
| 237 | + timeout: state.timeout |
| 238 | + }) |
| 239 | + else |
| 240 | + window.location = location.href |
| 241 | + } |
| 242 | +}) |
| 243 | + |
| 244 | + |
| 245 | +// Add the state property to jQuery's event object so we can use it in |
| 246 | +// $(window).bind('popstate') |
| 247 | +if ( $.inArray('state', $.event.props) < 0 ) |
| 248 | + $.event.props.push('state') |
| 249 | + |
| 250 | + |
| 251 | +// Is pjax supported by this browser? |
| 252 | +$.support.pjax = |
| 253 | + window.history && window.history.pushState && window.history.replaceState |
| 254 | + // pushState isn't reliable on iOS yet. |
| 255 | + && !navigator.userAgent.match(/(iPod|iPhone|iPad|WebApps\/.+CFNetwork)/) |
| 256 | + |
| 257 | + |
| 258 | +// Fall back to normalcy for older browsers. |
| 259 | +if ( !$.support.pjax ) { |
| 260 | + $.pjax = function( options ) { |
| 261 | + window.location = $.isFunction(options.url) ? options.url() : options.url |
| 262 | + } |
| 263 | + $.fn.pjax = function() { return this } |
| 264 | +} |
| 265 | + |
| 266 | +})(jQuery); |
0 commit comments