├── composer.json ├── src ├── views │ └── pjax.blade.php ├── EndaPjaxServiceProvider.php ├── EndaPjaxMiddleware.php └── pjax │ ├── css │ └── nprogress.css │ └── js │ ├── nprogress.js │ └── jquery.pjax.js ├── README.md └── LICENSE /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yuanchao/pjax-for-laravel-5", 3 | "description": "pjax for laravel 5.*", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "yuanchao", 8 | "email": "653069653@qq.com" 9 | } 10 | ], 11 | "minimum-stability": "dev", 12 | "require": { 13 | "symfony/dom-crawler": "2.7.*", 14 | "symfony/css-selector": "2.7.*" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "YuanChao\\Pjax\\":"src/" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/views/pjax.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/EndaPjaxServiceProvider.php: -------------------------------------------------------------------------------- 1 | 4 | * Time: 2015.10.28 下午12:01 5 | */ 6 | namespace YuanChao\Pjax; 7 | 8 | use Illuminate\Support\Facades\App; 9 | use Illuminate\Support\ServiceProvider; 10 | 11 | class EndaPjaxServiceProvider extends ServiceProvider 12 | { 13 | /** 14 | * Bootstrap the application services. 15 | * 16 | * @return void 17 | */ 18 | public function boot() 19 | { 20 | // 21 | 22 | $this->loadViewsFrom(__DIR__ . '/views', 'pjax'); 23 | 24 | $this->publishes([ 25 | __DIR__.'/views' => base_path('resources/views/vendor/pjax'), 26 | ]); 27 | 28 | $this->publishes([ 29 | __DIR__ . '/pjax' => base_path('public/plugins/pjax'), 30 | ]); 31 | } 32 | 33 | /** 34 | * Register the application services. 35 | * 36 | * @return void 37 | */ 38 | public function register() 39 | { 40 | 41 | } 42 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 在 Laravel 5.* 的版本中,使用 Pjax 实现无刷新效果,以及酷炫的进度条 2 | 3 | 4 | `求 star` 5 | 6 | `求 star` 7 | 8 | `求 star` 9 | 10 | ### 起因 11 | 12 | 群里面的朋友老是在问 laravel 怎么和 pjax 结合,于是今天早上答应了给大家写一篇文章,到准备写的时候,发现其实挺简单的,也没有多少可写的东西,于是乎,干脆直接封装成包,大家直接安装用就好了 13 | 14 | 15 | 感谢以下开源项目 16 | 17 | 1. jquery.pjax.js 18 | 19 | 2. nprogress 20 | 21 | 22 | 23 | ### 效果 24 | 25 | 26 | 27 | 28 | ### 安装 29 | 30 | 1.在 `composer.json` 的 require里 加入 31 | 32 | 33 | ``` 34 | "yuanchao/pjax-for-laravel-5": "dev-master" 35 | 36 | ``` 37 | 38 | 39 | 2.执行 `composer update` 40 | 41 | 42 | 3.在config/app.php 的 `providers` 数组加入一条 43 | 44 | 45 | ``` 46 | YuanChao\Pjax\EndaPjaxServiceProvider::class 47 | 48 | ``` 49 | 50 | 4.在 `Kernel` 的 `$middleware` 数组里添加 51 | 52 | ``` 53 | 54 | \YuanChao\Pjax\EndaPjaxMiddleware::class, 55 | 56 | ``` 57 | 58 | 59 | 5.执行 `php artisan vendor:publish` 60 | 61 | 62 | ### 使用 63 | 64 | `要先引入 jquery` 65 | 66 | 67 | 在`布局`文件中,插入以下代码 68 | 69 | ``` 70 | 71 | @include('pjax::pjax') 72 | 73 | 74 | ``` 75 | 76 | ### 交流 77 | 78 | 欢迎加入laravel学习小组交流:`365969825 ` 79 | 80 | 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 袁超 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/EndaPjaxMiddleware.php: -------------------------------------------------------------------------------- 1 | 4 | * Time: 2015.10.28 下午12:05 5 | */ 6 | 7 | namespace YuanChao\Pjax; 8 | 9 | use Closure; 10 | use Illuminate\Http\Request; 11 | use Illuminate\Http\Response; 12 | use Symfony\Component\DomCrawler\Crawler; 13 | 14 | class EndaPjaxMiddleware 15 | { 16 | 17 | public function handle($request, Closure $next) 18 | { 19 | $response = $next($request); 20 | if (!$response->isRedirection()) { 21 | if ($request->pjax()) { 22 | $crawler = new Crawler($response->getContent()); 23 | $response_title = $crawler->filter('head > title'); 24 | $response_container = $crawler->filter($request->header('X-PJAX-CONTAINER')); 25 | if ($response_container->count() != 0) { 26 | $title = ''; 27 | if ($response_title->count() != 0) { 28 | $title = '' . $response_title->html() . ''; 29 | } 30 | $response->setContent($title . $response_container->html()); 31 | } 32 | $response->header('X-PJAX-URL', $request->getRequestUri()); 33 | } 34 | } 35 | return $response; 36 | } 37 | } -------------------------------------------------------------------------------- /src/pjax/css/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: #29d; 8 | 9 | position: fixed; 10 | z-index: 1031; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d; 26 | opacity: 1.0; 27 | 28 | -webkit-transform: rotate(3deg) translate(0px, -4px); 29 | -ms-transform: rotate(3deg) translate(0px, -4px); 30 | transform: rotate(3deg) translate(0px, -4px); 31 | } 32 | 33 | /* Remove these to get rid of the spinner */ 34 | #nprogress .spinner { 35 | display: block; 36 | position: fixed; 37 | z-index: 1031; 38 | top: 15px; 39 | right: 15px; 40 | } 41 | 42 | #nprogress .spinner-icon { 43 | width: 18px; 44 | height: 18px; 45 | box-sizing: border-box; 46 | 47 | border: solid 2px transparent; 48 | border-top-color: #29d; 49 | border-left-color: #29d; 50 | border-radius: 50%; 51 | 52 | -webkit-animation: nprogress-spinner 400ms linear infinite; 53 | animation: nprogress-spinner 400ms linear infinite; 54 | } 55 | 56 | .nprogress-custom-parent { 57 | overflow: hidden; 58 | position: relative; 59 | } 60 | 61 | .nprogress-custom-parent #nprogress .spinner, 62 | .nprogress-custom-parent #nprogress .bar { 63 | position: absolute; 64 | } 65 | 66 | @-webkit-keyframes nprogress-spinner { 67 | 0% { -webkit-transform: rotate(0deg); } 68 | 100% { -webkit-transform: rotate(360deg); } 69 | } 70 | @keyframes nprogress-spinner { 71 | 0% { transform: rotate(0deg); } 72 | 100% { transform: rotate(360deg); } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/pjax/js/nprogress.js: -------------------------------------------------------------------------------- 1 | /* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress 2 | * @license MIT */ 3 | 4 | ;(function(root, factory) { 5 | 6 | if (typeof define === 'function' && define.amd) { 7 | define(factory); 8 | } else if (typeof exports === 'object') { 9 | module.exports = factory(); 10 | } else { 11 | root.NProgress = factory(); 12 | } 13 | 14 | })(this, function() { 15 | var NProgress = {}; 16 | 17 | NProgress.version = '0.2.0'; 18 | 19 | var Settings = NProgress.settings = { 20 | minimum: 0.08, 21 | easing: 'ease', 22 | positionUsing: '', 23 | speed: 200, 24 | trickle: true, 25 | trickleRate: 0.02, 26 | trickleSpeed: 800, 27 | showSpinner: true, 28 | barSelector: '[role="bar"]', 29 | spinnerSelector: '[role="spinner"]', 30 | parent: 'body', 31 | template: '
' 32 | }; 33 | 34 | /** 35 | * Updates configuration. 36 | * 37 | * NProgress.configure({ 38 | * minimum: 0.1 39 | * }); 40 | */ 41 | NProgress.configure = function(options) { 42 | var key, value; 43 | for (key in options) { 44 | value = options[key]; 45 | if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value; 46 | } 47 | 48 | return this; 49 | }; 50 | 51 | /** 52 | * Last number. 53 | */ 54 | 55 | NProgress.status = null; 56 | 57 | /** 58 | * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`. 59 | * 60 | * NProgress.set(0.4); 61 | * NProgress.set(1.0); 62 | */ 63 | 64 | NProgress.set = function(n) { 65 | var started = NProgress.isStarted(); 66 | 67 | n = clamp(n, Settings.minimum, 1); 68 | NProgress.status = (n === 1 ? null : n); 69 | 70 | var progress = NProgress.render(!started), 71 | bar = progress.querySelector(Settings.barSelector), 72 | speed = Settings.speed, 73 | ease = Settings.easing; 74 | 75 | progress.offsetWidth; /* Repaint */ 76 | 77 | queue(function(next) { 78 | // Set positionUsing if it hasn't already been set 79 | if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS(); 80 | 81 | // Add transition 82 | css(bar, barPositionCSS(n, speed, ease)); 83 | 84 | if (n === 1) { 85 | // Fade out 86 | css(progress, { 87 | transition: 'none', 88 | opacity: 1 89 | }); 90 | progress.offsetWidth; /* Repaint */ 91 | 92 | setTimeout(function() { 93 | css(progress, { 94 | transition: 'all ' + speed + 'ms linear', 95 | opacity: 0 96 | }); 97 | setTimeout(function() { 98 | NProgress.remove(); 99 | next(); 100 | }, speed); 101 | }, speed); 102 | } else { 103 | setTimeout(next, speed); 104 | } 105 | }); 106 | 107 | return this; 108 | }; 109 | 110 | NProgress.isStarted = function() { 111 | return typeof NProgress.status === 'number'; 112 | }; 113 | 114 | /** 115 | * Shows the progress bar. 116 | * This is the same as setting the status to 0%, except that it doesn't go backwards. 117 | * 118 | * NProgress.start(); 119 | * 120 | */ 121 | NProgress.start = function() { 122 | if (!NProgress.status) NProgress.set(0); 123 | 124 | var work = function() { 125 | setTimeout(function() { 126 | if (!NProgress.status) return; 127 | NProgress.trickle(); 128 | work(); 129 | }, Settings.trickleSpeed); 130 | }; 131 | 132 | if (Settings.trickle) work(); 133 | 134 | return this; 135 | }; 136 | 137 | /** 138 | * Hides the progress bar. 139 | * This is the *sort of* the same as setting the status to 100%, with the 140 | * difference being `done()` makes some placebo effect of some realistic motion. 141 | * 142 | * NProgress.done(); 143 | * 144 | * If `true` is passed, it will show the progress bar even if its hidden. 145 | * 146 | * NProgress.done(true); 147 | */ 148 | 149 | NProgress.done = function(force) { 150 | if (!force && !NProgress.status) return this; 151 | 152 | return NProgress.inc(0.3 + 0.5 * Math.random()).set(1); 153 | }; 154 | 155 | /** 156 | * Increments by a random amount. 157 | */ 158 | 159 | NProgress.inc = function(amount) { 160 | var n = NProgress.status; 161 | 162 | if (!n) { 163 | return NProgress.start(); 164 | } else { 165 | if (typeof amount !== 'number') { 166 | amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95); 167 | } 168 | 169 | n = clamp(n + amount, 0, 0.994); 170 | return NProgress.set(n); 171 | } 172 | }; 173 | 174 | NProgress.trickle = function() { 175 | return NProgress.inc(Math.random() * Settings.trickleRate); 176 | }; 177 | 178 | /** 179 | * Waits for all supplied jQuery promises and 180 | * increases the progress as the promises resolve. 181 | * 182 | * @param $promise jQUery Promise 183 | */ 184 | (function() { 185 | var initial = 0, current = 0; 186 | 187 | NProgress.promise = function($promise) { 188 | if (!$promise || $promise.state() === "resolved") { 189 | return this; 190 | } 191 | 192 | if (current === 0) { 193 | NProgress.start(); 194 | } 195 | 196 | initial++; 197 | current++; 198 | 199 | $promise.always(function() { 200 | current--; 201 | if (current === 0) { 202 | initial = 0; 203 | NProgress.done(); 204 | } else { 205 | NProgress.set((initial - current) / initial); 206 | } 207 | }); 208 | 209 | return this; 210 | }; 211 | 212 | })(); 213 | 214 | /** 215 | * (Internal) renders the progress bar markup based on the `template` 216 | * setting. 217 | */ 218 | 219 | NProgress.render = function(fromStart) { 220 | if (NProgress.isRendered()) return document.getElementById('nprogress'); 221 | 222 | addClass(document.documentElement, 'nprogress-busy'); 223 | 224 | var progress = document.createElement('div'); 225 | progress.id = 'nprogress'; 226 | progress.innerHTML = Settings.template; 227 | 228 | var bar = progress.querySelector(Settings.barSelector), 229 | perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0), 230 | parent = document.querySelector(Settings.parent), 231 | spinner; 232 | 233 | css(bar, { 234 | transition: 'all 0 linear', 235 | transform: 'translate3d(' + perc + '%,0,0)' 236 | }); 237 | 238 | if (!Settings.showSpinner) { 239 | spinner = progress.querySelector(Settings.spinnerSelector); 240 | spinner && removeElement(spinner); 241 | } 242 | 243 | if (parent != document.body) { 244 | addClass(parent, 'nprogress-custom-parent'); 245 | } 246 | 247 | parent.appendChild(progress); 248 | return progress; 249 | }; 250 | 251 | /** 252 | * Removes the element. Opposite of render(). 253 | */ 254 | 255 | NProgress.remove = function() { 256 | removeClass(document.documentElement, 'nprogress-busy'); 257 | removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent'); 258 | var progress = document.getElementById('nprogress'); 259 | progress && removeElement(progress); 260 | }; 261 | 262 | /** 263 | * Checks if the progress bar is rendered. 264 | */ 265 | 266 | NProgress.isRendered = function() { 267 | return !!document.getElementById('nprogress'); 268 | }; 269 | 270 | /** 271 | * Determine which positioning CSS rule to use. 272 | */ 273 | 274 | NProgress.getPositioningCSS = function() { 275 | // Sniff on document.body.style 276 | var bodyStyle = document.body.style; 277 | 278 | // Sniff prefixes 279 | var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' : 280 | ('MozTransform' in bodyStyle) ? 'Moz' : 281 | ('msTransform' in bodyStyle) ? 'ms' : 282 | ('OTransform' in bodyStyle) ? 'O' : ''; 283 | 284 | if (vendorPrefix + 'Perspective' in bodyStyle) { 285 | // Modern browsers with 3D support, e.g. Webkit, IE10 286 | return 'translate3d'; 287 | } else if (vendorPrefix + 'Transform' in bodyStyle) { 288 | // Browsers without 3D support, e.g. IE9 289 | return 'translate'; 290 | } else { 291 | // Browsers without translate() support, e.g. IE7-8 292 | return 'margin'; 293 | } 294 | }; 295 | 296 | /** 297 | * Helpers 298 | */ 299 | 300 | function clamp(n, min, max) { 301 | if (n < min) return min; 302 | if (n > max) return max; 303 | return n; 304 | } 305 | 306 | /** 307 | * (Internal) converts a percentage (`0..1`) to a bar translateX 308 | * percentage (`-100%..0%`). 309 | */ 310 | 311 | function toBarPerc(n) { 312 | return (-1 + n) * 100; 313 | } 314 | 315 | 316 | /** 317 | * (Internal) returns the correct CSS for changing the bar's 318 | * position given an n percentage, and speed and ease from Settings 319 | */ 320 | 321 | function barPositionCSS(n, speed, ease) { 322 | var barCSS; 323 | 324 | if (Settings.positionUsing === 'translate3d') { 325 | barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' }; 326 | } else if (Settings.positionUsing === 'translate') { 327 | barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' }; 328 | } else { 329 | barCSS = { 'margin-left': toBarPerc(n)+'%' }; 330 | } 331 | 332 | barCSS.transition = 'all '+speed+'ms '+ease; 333 | 334 | return barCSS; 335 | } 336 | 337 | /** 338 | * (Internal) Queues a function to be executed. 339 | */ 340 | 341 | var queue = (function() { 342 | var pending = []; 343 | 344 | function next() { 345 | var fn = pending.shift(); 346 | if (fn) { 347 | fn(next); 348 | } 349 | } 350 | 351 | return function(fn) { 352 | pending.push(fn); 353 | if (pending.length == 1) next(); 354 | }; 355 | })(); 356 | 357 | /** 358 | * (Internal) Applies css properties to an element, similar to the jQuery 359 | * css method. 360 | * 361 | * While this helper does assist with vendor prefixed property names, it 362 | * does not perform any manipulation of values prior to setting styles. 363 | */ 364 | 365 | var css = (function() { 366 | var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ], 367 | cssProps = {}; 368 | 369 | function camelCase(string) { 370 | return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) { 371 | return letter.toUpperCase(); 372 | }); 373 | } 374 | 375 | function getVendorProp(name) { 376 | var style = document.body.style; 377 | if (name in style) return name; 378 | 379 | var i = cssPrefixes.length, 380 | capName = name.charAt(0).toUpperCase() + name.slice(1), 381 | vendorName; 382 | while (i--) { 383 | vendorName = cssPrefixes[i] + capName; 384 | if (vendorName in style) return vendorName; 385 | } 386 | 387 | return name; 388 | } 389 | 390 | function getStyleProp(name) { 391 | name = camelCase(name); 392 | return cssProps[name] || (cssProps[name] = getVendorProp(name)); 393 | } 394 | 395 | function applyCss(element, prop, value) { 396 | prop = getStyleProp(prop); 397 | element.style[prop] = value; 398 | } 399 | 400 | return function(element, properties) { 401 | var args = arguments, 402 | prop, 403 | value; 404 | 405 | if (args.length == 2) { 406 | for (prop in properties) { 407 | value = properties[prop]; 408 | if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value); 409 | } 410 | } else { 411 | applyCss(element, args[1], args[2]); 412 | } 413 | } 414 | })(); 415 | 416 | /** 417 | * (Internal) Determines if an element or space separated list of class names contains a class name. 418 | */ 419 | 420 | function hasClass(element, name) { 421 | var list = typeof element == 'string' ? element : classList(element); 422 | return list.indexOf(' ' + name + ' ') >= 0; 423 | } 424 | 425 | /** 426 | * (Internal) Adds a class to an element. 427 | */ 428 | 429 | function addClass(element, name) { 430 | var oldList = classList(element), 431 | newList = oldList + name; 432 | 433 | if (hasClass(oldList, name)) return; 434 | 435 | // Trim the opening space. 436 | element.className = newList.substring(1); 437 | } 438 | 439 | /** 440 | * (Internal) Removes a class from an element. 441 | */ 442 | 443 | function removeClass(element, name) { 444 | var oldList = classList(element), 445 | newList; 446 | 447 | if (!hasClass(element, name)) return; 448 | 449 | // Replace the class name. 450 | newList = oldList.replace(' ' + name + ' ', ' '); 451 | 452 | // Trim the opening and closing spaces. 453 | element.className = newList.substring(1, newList.length - 1); 454 | } 455 | 456 | /** 457 | * (Internal) Gets a space separated list of the class names on the element. 458 | * The list is wrapped with a single space on each end to facilitate finding 459 | * matches within the list. 460 | */ 461 | 462 | function classList(element) { 463 | return (' ' + (element.className || '') + ' ').replace(/\s+/gi, ' '); 464 | } 465 | 466 | /** 467 | * (Internal) Removes an element from the DOM. 468 | */ 469 | 470 | function removeElement(element) { 471 | element && element.parentNode && element.parentNode.removeChild(element); 472 | } 473 | 474 | return NProgress; 475 | }); 476 | 477 | -------------------------------------------------------------------------------- /src/pjax/js/jquery.pjax.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2012, Chris Wanstrath 3 | * Released under the MIT License 4 | * https://github.com/defunkt/jquery-pjax 5 | */ 6 | 7 | (function($){ 8 | 9 | // When called on a container with a selector, fetches the href with 10 | // ajax into the container or with the data-pjax attribute on the link 11 | // itself. 12 | // 13 | // Tries to make sure the back button and ctrl+click work the way 14 | // you'd expect. 15 | // 16 | // Exported as $.fn.pjax 17 | // 18 | // Accepts a jQuery ajax options object that may include these 19 | // pjax specific options: 20 | // 21 | // 22 | // container - Where to stick the response body. Usually a String selector. 23 | // $(container).html(xhr.responseBody) 24 | // (default: current jquery context) 25 | // push - Whether to pushState the URL. Defaults to true (of course). 26 | // replace - Want to use replaceState instead? That's cool. 27 | // 28 | // For convenience the second parameter can be either the container or 29 | // the options object. 30 | // 31 | // Returns the jQuery object 32 | function fnPjax(selector, container, options) { 33 | var context = this 34 | return this.on('click.pjax', selector, function(event) { 35 | var opts = $.extend({}, optionsFor(container, options)) 36 | if (!opts.container) 37 | opts.container = $(this).attr('data-pjax') || context 38 | handleClick(event, opts) 39 | }) 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 | 64 | var link = event.currentTarget 65 | 66 | if (link.tagName.toUpperCase() !== 'A') 67 | throw "$.fn.pjax or $.pjax.click requires an anchor element" 68 | 69 | // Middle click, cmd click, and ctrl click should open 70 | // links in a new tab as normal. 71 | if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) 72 | return 73 | 74 | // Ignore cross origin links 75 | if ( location.protocol !== link.protocol || location.hostname !== link.hostname ) 76 | return 77 | 78 | // Ignore case when a hash is being tacked on the current URL 79 | if ( link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location) ) 80 | return 81 | 82 | // Ignore event with default prevented 83 | if (event.isDefaultPrevented()) 84 | return 85 | 86 | var defaults = { 87 | url: link.href, 88 | container: $(link).attr('data-pjax'), 89 | target: link 90 | } 91 | 92 | var opts = $.extend({}, defaults, options) 93 | var clickEvent = $.Event('pjax:click') 94 | $(link).trigger(clickEvent, [opts]) 95 | 96 | if (!clickEvent.isDefaultPrevented()) { 97 | pjax(opts) 98 | event.preventDefault() 99 | $(link).trigger('pjax:clicked', [opts]) 100 | } 101 | } 102 | 103 | // Public: pjax on form submit handler 104 | // 105 | // Exported as $.pjax.submit 106 | // 107 | // event - "click" jQuery.Event 108 | // options - pjax options 109 | // 110 | // Examples 111 | // 112 | // $(document).on('submit', 'form', function(event) { 113 | // var container = $(this).closest('[data-pjax-container]') 114 | // $.pjax.submit(event, container) 115 | // }) 116 | // 117 | // Returns nothing. 118 | function handleSubmit(event, container, options) { 119 | options = optionsFor(container, options) 120 | 121 | var form = event.currentTarget 122 | var $form = $(form) 123 | 124 | if (form.tagName.toUpperCase() !== 'FORM') 125 | throw "$.pjax.submit requires a form element" 126 | 127 | var defaults = { 128 | type: ($form.attr('method') || 'GET').toUpperCase(), 129 | url: $form.attr('action'), 130 | container: $form.attr('data-pjax'), 131 | target: form 132 | } 133 | 134 | if (defaults.type !== 'GET' && window.FormData !== undefined) { 135 | defaults.data = new FormData(form); 136 | defaults.processData = false; 137 | defaults.contentType = false; 138 | } else { 139 | // Can't handle file uploads, exit 140 | if ($(form).find(':file').length) { 141 | return; 142 | } 143 | 144 | // Fallback to manually serializing the fields 145 | defaults.data = $(form).serializeArray(); 146 | } 147 | 148 | pjax($.extend({}, defaults, options)) 149 | 150 | event.preventDefault() 151 | } 152 | 153 | // Loads a URL with ajax, puts the response body inside a container, 154 | // then pushState()'s the loaded URL. 155 | // 156 | // Works just like $.ajax in that it accepts a jQuery ajax 157 | // settings object (with keys like url, type, data, etc). 158 | // 159 | // Accepts these extra keys: 160 | // 161 | // container - Where to stick the response body. 162 | // $(container).html(xhr.responseBody) 163 | // push - Whether to pushState the URL. Defaults to true (of course). 164 | // replace - Want to use replaceState instead? That's cool. 165 | // 166 | // Use it just like $.ajax: 167 | // 168 | // var xhr = $.pjax({ url: this.href, container: '#main' }) 169 | // console.log( xhr.readyState ) 170 | // 171 | // Returns whatever $.ajax returns. 172 | function pjax(options) { 173 | options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options) 174 | 175 | if ($.isFunction(options.url)) { 176 | options.url = options.url() 177 | } 178 | 179 | var target = options.target 180 | 181 | var hash = parseURL(options.url).hash 182 | 183 | var context = options.context = findContainerFor(options.container) 184 | 185 | // We want the browser to maintain two separate internal caches: one 186 | // for pjax'd partial page loads and one for normal page loads. 187 | // Without adding this secret parameter, some browsers will often 188 | // confuse the two. 189 | if (!options.data) options.data = {} 190 | if ($.isArray(options.data)) { 191 | options.data.push({name: '_pjax', value: context.selector}) 192 | } else { 193 | options.data._pjax = context.selector 194 | } 195 | 196 | function fire(type, args, props) { 197 | if (!props) props = {} 198 | props.relatedTarget = target 199 | var event = $.Event(type, props) 200 | context.trigger(event, args) 201 | return !event.isDefaultPrevented() 202 | } 203 | 204 | var timeoutTimer 205 | 206 | options.beforeSend = function(xhr, settings) { 207 | // No timeout for non-GET requests 208 | // Its not safe to request the resource again with a fallback method. 209 | if (settings.type !== 'GET') { 210 | settings.timeout = 0 211 | } 212 | 213 | xhr.setRequestHeader('X-PJAX', 'true') 214 | xhr.setRequestHeader('X-PJAX-Container', context.selector) 215 | 216 | if (!fire('pjax:beforeSend', [xhr, settings])) 217 | return false 218 | 219 | if (settings.timeout > 0) { 220 | timeoutTimer = setTimeout(function() { 221 | if (fire('pjax:timeout', [xhr, options])) 222 | xhr.abort('timeout') 223 | }, settings.timeout) 224 | 225 | // Clear timeout setting so jquerys internal timeout isn't invoked 226 | settings.timeout = 0 227 | } 228 | 229 | var url = parseURL(settings.url) 230 | if (hash) url.hash = hash 231 | options.requestUrl = stripInternalParams(url) 232 | } 233 | 234 | options.complete = function(xhr, textStatus) { 235 | if (timeoutTimer) 236 | clearTimeout(timeoutTimer) 237 | 238 | fire('pjax:complete', [xhr, textStatus, options]) 239 | 240 | fire('pjax:end', [xhr, options]) 241 | } 242 | 243 | options.error = function(xhr, textStatus, errorThrown) { 244 | var container = extractContainer("", xhr, options) 245 | 246 | var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]) 247 | if (options.type == 'GET' && textStatus !== 'abort' && allowed) { 248 | locationReplace(container.url) 249 | } 250 | } 251 | 252 | options.success = function(data, status, xhr) { 253 | var previousState = pjax.state; 254 | 255 | // If $.pjax.defaults.version is a function, invoke it first. 256 | // Otherwise it can be a static string. 257 | var currentVersion = (typeof $.pjax.defaults.version === 'function') ? 258 | $.pjax.defaults.version() : 259 | $.pjax.defaults.version 260 | 261 | var latestVersion = xhr.getResponseHeader('X-PJAX-Version') 262 | 263 | var container = extractContainer(data, xhr, options) 264 | 265 | var url = parseURL(container.url) 266 | if (hash) { 267 | url.hash = hash 268 | container.url = url.href 269 | } 270 | 271 | // If there is a layout version mismatch, hard load the new url 272 | if (currentVersion && latestVersion && currentVersion !== latestVersion) { 273 | locationReplace(container.url) 274 | return 275 | } 276 | 277 | // If the new response is missing a body, hard load the page 278 | if (!container.contents) { 279 | locationReplace(container.url) 280 | return 281 | } 282 | 283 | pjax.state = { 284 | id: options.id || uniqueId(), 285 | url: container.url, 286 | title: container.title, 287 | container: context.selector, 288 | fragment: options.fragment, 289 | timeout: options.timeout 290 | } 291 | 292 | if (options.push || options.replace) { 293 | window.history.replaceState(pjax.state, container.title, container.url) 294 | } 295 | 296 | // Only blur the focus if the focused element is within the container. 297 | var blurFocus = $.contains(options.container, document.activeElement) 298 | 299 | // Clear out any focused controls before inserting new page contents. 300 | if (blurFocus) { 301 | try { 302 | document.activeElement.blur() 303 | } catch (e) { } 304 | } 305 | 306 | if (container.title) document.title = container.title 307 | 308 | fire('pjax:beforeReplace', [container.contents, options], { 309 | state: pjax.state, 310 | previousState: previousState 311 | }) 312 | context.html(container.contents) 313 | 314 | // FF bug: Won't autofocus fields that are inserted via JS. 315 | // This behavior is incorrect. So if theres no current focus, autofocus 316 | // the last field. 317 | // 318 | // http://www.w3.org/html/wg/drafts/html/master/forms.html 319 | var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0] 320 | if (autofocusEl && document.activeElement !== autofocusEl) { 321 | autofocusEl.focus(); 322 | } 323 | 324 | executeScriptTags(container.scripts) 325 | 326 | var scrollTo = options.scrollTo 327 | 328 | // Ensure browser scrolls to the element referenced by the URL anchor 329 | if (hash) { 330 | var name = decodeURIComponent(hash.slice(1)) 331 | var target = document.getElementById(name) || document.getElementsByName(name)[0] 332 | if (target) scrollTo = $(target).offset().top 333 | } 334 | 335 | if (typeof scrollTo == 'number') $(window).scrollTop(scrollTo) 336 | 337 | fire('pjax:success', [data, status, xhr, options]) 338 | } 339 | 340 | 341 | // Initialize pjax.state for the initial page load. Assume we're 342 | // using the container and options of the link we're loading for the 343 | // back button to the initial page. This ensures good back button 344 | // behavior. 345 | if (!pjax.state) { 346 | pjax.state = { 347 | id: uniqueId(), 348 | url: window.location.href, 349 | title: document.title, 350 | container: context.selector, 351 | fragment: options.fragment, 352 | timeout: options.timeout 353 | } 354 | window.history.replaceState(pjax.state, document.title) 355 | } 356 | 357 | // Cancel the current request if we're already pjaxing 358 | abortXHR(pjax.xhr) 359 | 360 | pjax.options = options 361 | var xhr = pjax.xhr = $.ajax(options) 362 | 363 | if (xhr.readyState > 0) { 364 | if (options.push && !options.replace) { 365 | // Cache current container element before replacing it 366 | cachePush(pjax.state.id, cloneContents(context)) 367 | 368 | window.history.pushState(null, "", options.requestUrl) 369 | } 370 | 371 | fire('pjax:start', [xhr, options]) 372 | fire('pjax:send', [xhr, options]) 373 | } 374 | 375 | return pjax.xhr 376 | } 377 | 378 | // Public: Reload current page with pjax. 379 | // 380 | // Returns whatever $.pjax returns. 381 | function pjaxReload(container, options) { 382 | var defaults = { 383 | url: window.location.href, 384 | push: false, 385 | replace: true, 386 | scrollTo: false 387 | } 388 | 389 | return pjax($.extend(defaults, optionsFor(container, options))) 390 | } 391 | 392 | // Internal: Hard replace current state with url. 393 | // 394 | // Work for around WebKit 395 | // https://bugs.webkit.org/show_bug.cgi?id=93506 396 | // 397 | // Returns nothing. 398 | function locationReplace(url) { 399 | window.history.replaceState(null, "", pjax.state.url) 400 | window.location.replace(url) 401 | } 402 | 403 | 404 | var initialPop = true 405 | var initialURL = window.location.href 406 | var initialState = window.history.state 407 | 408 | // Initialize $.pjax.state if possible 409 | // Happens when reloading a page and coming forward from a different 410 | // session history. 411 | if (initialState && initialState.container) { 412 | pjax.state = initialState 413 | } 414 | 415 | // Non-webkit browsers don't fire an initial popstate event 416 | if ('state' in window.history) { 417 | initialPop = false 418 | } 419 | 420 | // popstate handler takes care of the back and forward buttons 421 | // 422 | // You probably shouldn't use pjax on pages with other pushState 423 | // stuff yet. 424 | function onPjaxPopstate(event) { 425 | 426 | // Hitting back or forward should override any pending PJAX request. 427 | if (!initialPop) { 428 | abortXHR(pjax.xhr) 429 | } 430 | 431 | var previousState = pjax.state 432 | var state = event.state 433 | var direction 434 | 435 | if (state && state.container) { 436 | // When coming forward from a separate history session, will get an 437 | // initial pop with a state we are already at. Skip reloading the current 438 | // page. 439 | if (initialPop && initialURL == state.url) return 440 | 441 | if (previousState) { 442 | // If popping back to the same state, just skip. 443 | // Could be clicking back from hashchange rather than a pushState. 444 | if (previousState.id === state.id) return 445 | 446 | // Since state IDs always increase, we can deduce the navigation direction 447 | direction = previousState.id < state.id ? 'forward' : 'back' 448 | } 449 | 450 | var cache = cacheMapping[state.id] || [] 451 | var container = $(cache[0] || state.container), contents = cache[1] 452 | 453 | if (container.length) { 454 | if (previousState) { 455 | // Cache current container before replacement and inform the 456 | // cache which direction the history shifted. 457 | cachePop(direction, previousState.id, cloneContents(container)) 458 | } 459 | 460 | var popstateEvent = $.Event('pjax:popstate', { 461 | state: state, 462 | direction: direction 463 | }) 464 | container.trigger(popstateEvent) 465 | 466 | var options = { 467 | id: state.id, 468 | url: state.url, 469 | container: container, 470 | push: false, 471 | fragment: state.fragment, 472 | timeout: state.timeout, 473 | scrollTo: false 474 | } 475 | 476 | if (contents) { 477 | container.trigger('pjax:start', [null, options]) 478 | 479 | pjax.state = state 480 | if (state.title) document.title = state.title 481 | var beforeReplaceEvent = $.Event('pjax:beforeReplace', { 482 | state: state, 483 | previousState: previousState 484 | }) 485 | container.trigger(beforeReplaceEvent, [contents, options]) 486 | container.html(contents) 487 | 488 | container.trigger('pjax:end', [null, options]) 489 | } else { 490 | pjax(options) 491 | } 492 | 493 | // Force reflow/relayout before the browser tries to restore the 494 | // scroll position. 495 | container[0].offsetHeight 496 | } else { 497 | locationReplace(location.href) 498 | } 499 | } 500 | initialPop = false 501 | } 502 | 503 | // Fallback version of main pjax function for browsers that don't 504 | // support pushState. 505 | // 506 | // Returns nothing since it retriggers a hard form submission. 507 | function fallbackPjax(options) { 508 | var url = $.isFunction(options.url) ? options.url() : options.url, 509 | method = options.type ? options.type.toUpperCase() : 'GET' 510 | 511 | var form = $('
', { 512 | method: method === 'GET' ? 'GET' : 'POST', 513 | action: url, 514 | style: 'display:none' 515 | }) 516 | 517 | if (method !== 'GET' && method !== 'POST') { 518 | form.append($('', { 519 | type: 'hidden', 520 | name: '_method', 521 | value: method.toLowerCase() 522 | })) 523 | } 524 | 525 | var data = options.data 526 | if (typeof data === 'string') { 527 | $.each(data.split('&'), function(index, value) { 528 | var pair = value.split('=') 529 | form.append($('', {type: 'hidden', name: pair[0], value: pair[1]})) 530 | }) 531 | } else if ($.isArray(data)) { 532 | $.each(data, function(index, value) { 533 | form.append($('', {type: 'hidden', name: value.name, value: value.value})) 534 | }) 535 | } else if (typeof data === 'object') { 536 | var key 537 | for (key in data) 538 | form.append($('', {type: 'hidden', name: key, value: data[key]})) 539 | } 540 | 541 | $(document.body).append(form) 542 | form.submit() 543 | } 544 | 545 | // Internal: Abort an XmlHttpRequest if it hasn't been completed, 546 | // also removing its event handlers. 547 | function abortXHR(xhr) { 548 | if ( xhr && xhr.readyState < 4) { 549 | xhr.onreadystatechange = $.noop 550 | xhr.abort() 551 | } 552 | } 553 | 554 | // Internal: Generate unique id for state object. 555 | // 556 | // Use a timestamp instead of a counter since ids should still be 557 | // unique across page loads. 558 | // 559 | // Returns Number. 560 | function uniqueId() { 561 | return (new Date).getTime() 562 | } 563 | 564 | function cloneContents(container) { 565 | var cloned = container.clone() 566 | // Unmark script tags as already being eval'd so they can get executed again 567 | // when restored from cache. HAXX: Uses jQuery internal method. 568 | cloned.find('script').each(function(){ 569 | if (!this.src) jQuery._data(this, 'globalEval', false) 570 | }) 571 | return [container.selector, cloned.contents()] 572 | } 573 | 574 | // Internal: Strip internal query params from parsed URL. 575 | // 576 | // Returns sanitized url.href String. 577 | function stripInternalParams(url) { 578 | url.search = url.search.replace(/([?&])(_pjax|_)=[^&]*/g, '') 579 | return url.href.replace(/\?($|#)/, '$1') 580 | } 581 | 582 | // Internal: Parse URL components and returns a Locationish object. 583 | // 584 | // url - String URL 585 | // 586 | // Returns HTMLAnchorElement that acts like Location. 587 | function parseURL(url) { 588 | var a = document.createElement('a') 589 | a.href = url 590 | return a 591 | } 592 | 593 | // Internal: Return the `href` component of given URL object with the hash 594 | // portion removed. 595 | // 596 | // location - Location or HTMLAnchorElement 597 | // 598 | // Returns String 599 | function stripHash(location) { 600 | return location.href.replace(/#.*/, '') 601 | } 602 | 603 | // Internal: Build options Object for arguments. 604 | // 605 | // For convenience the first parameter can be either the container or 606 | // the options object. 607 | // 608 | // Examples 609 | // 610 | // optionsFor('#container') 611 | // // => {container: '#container'} 612 | // 613 | // optionsFor('#container', {push: true}) 614 | // // => {container: '#container', push: true} 615 | // 616 | // optionsFor({container: '#container', push: true}) 617 | // // => {container: '#container', push: true} 618 | // 619 | // Returns options Object. 620 | function optionsFor(container, options) { 621 | // Both container and options 622 | if ( container && options ) 623 | options.container = container 624 | 625 | // First argument is options Object 626 | else if ( $.isPlainObject(container) ) 627 | options = container 628 | 629 | // Only container 630 | else 631 | options = {container: container} 632 | 633 | // Find and validate container 634 | if (options.container) 635 | options.container = findContainerFor(options.container) 636 | 637 | return options 638 | } 639 | 640 | // Internal: Find container element for a variety of inputs. 641 | // 642 | // Because we can't persist elements using the history API, we must be 643 | // able to find a String selector that will consistently find the Element. 644 | // 645 | // container - A selector String, jQuery object, or DOM Element. 646 | // 647 | // Returns a jQuery object whose context is `document` and has a selector. 648 | function findContainerFor(container) { 649 | container = $(container) 650 | 651 | if ( !container.length ) { 652 | throw "no pjax container for " + container.selector 653 | } else if ( container.selector !== '' && container.context === document ) { 654 | return container 655 | } else if ( container.attr('id') ) { 656 | return $('#' + container.attr('id')) 657 | } else { 658 | throw "cant get selector for pjax container!" 659 | } 660 | } 661 | 662 | // Internal: Filter and find all elements matching the selector. 663 | // 664 | // Where $.fn.find only matches descendants, findAll will test all the 665 | // top level elements in the jQuery object as well. 666 | // 667 | // elems - jQuery object of Elements 668 | // selector - String selector to match 669 | // 670 | // Returns a jQuery object. 671 | function findAll(elems, selector) { 672 | return elems.filter(selector).add(elems.find(selector)); 673 | } 674 | 675 | function parseHTML(html) { 676 | return $.parseHTML(html, document, true) 677 | } 678 | 679 | // Internal: Extracts container and metadata from response. 680 | // 681 | // 1. Extracts X-PJAX-URL header if set 682 | // 2. Extracts inline tags 683 | // 3. Builds response Element and extracts fragment if set 684 | // 685 | // data - String response data 686 | // xhr - XHR response 687 | // options - pjax options Object 688 | // 689 | // Returns an Object with url, title, and contents keys. 690 | function extractContainer(data, xhr, options) { 691 | var obj = {}, fullDocument = /<html/i.test(data) 692 | 693 | // Prefer X-PJAX-URL header if it was set, otherwise fallback to 694 | // using the original requested url. 695 | var serverUrl = xhr.getResponseHeader('X-PJAX-URL') 696 | obj.url = serverUrl ? stripInternalParams(parseURL(serverUrl)) : options.requestUrl 697 | 698 | // Attempt to parse response html into elements 699 | if (fullDocument) { 700 | var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0])) 701 | var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0])) 702 | } else { 703 | var $head = $body = $(parseHTML(data)) 704 | } 705 | 706 | // If response data is empty, return fast 707 | if ($body.length === 0) 708 | return obj 709 | 710 | // If there's a <title> tag in the header, use it as 711 | // the page's title. 712 | obj.title = findAll($head, 'title').last().text() 713 | 714 | if (options.fragment) { 715 | // If they specified a fragment, look for it in the response 716 | // and pull it out. 717 | if (options.fragment === 'body') { 718 | var $fragment = $body 719 | } else { 720 | var $fragment = findAll($body, options.fragment).first() 721 | } 722 | 723 | if ($fragment.length) { 724 | obj.contents = options.fragment === 'body' ? $fragment : $fragment.contents() 725 | 726 | // If there's no title, look for data-title and title attributes 727 | // on the fragment 728 | if (!obj.title) 729 | obj.title = $fragment.attr('title') || $fragment.data('title') 730 | } 731 | 732 | } else if (!fullDocument) { 733 | obj.contents = $body 734 | } 735 | 736 | // Clean up any <title> tags 737 | if (obj.contents) { 738 | // Remove any parent title elements 739 | obj.contents = obj.contents.not(function() { return $(this).is('title') }) 740 | 741 | // Then scrub any titles from their descendants 742 | obj.contents.find('title').remove() 743 | 744 | // Gather all script[src] elements 745 | obj.scripts = findAll(obj.contents, 'script[src]').remove() 746 | obj.contents = obj.contents.not(obj.scripts) 747 | } 748 | 749 | // Trim any whitespace off the title 750 | if (obj.title) obj.title = $.trim(obj.title) 751 | 752 | return obj 753 | } 754 | 755 | // Load an execute scripts using standard script request. 756 | // 757 | // Avoids jQuery's traditional $.getScript which does a XHR request and 758 | // globalEval. 759 | // 760 | // scripts - jQuery object of script Elements 761 | // 762 | // Returns nothing. 763 | function executeScriptTags(scripts) { 764 | if (!scripts) return 765 | 766 | var existingScripts = $('script[src]') 767 | 768 | scripts.each(function() { 769 | var src = this.src 770 | var matchedScripts = existingScripts.filter(function() { 771 | return this.src === src 772 | }) 773 | if (matchedScripts.length) return 774 | 775 | var script = document.createElement('script') 776 | var type = $(this).attr('type') 777 | if (type) script.type = type 778 | script.src = $(this).attr('src') 779 | document.head.appendChild(script) 780 | }) 781 | } 782 | 783 | // Internal: History DOM caching class. 784 | var cacheMapping = {} 785 | var cacheForwardStack = [] 786 | var cacheBackStack = [] 787 | 788 | // Push previous state id and container contents into the history 789 | // cache. Should be called in conjunction with `pushState` to save the 790 | // previous container contents. 791 | // 792 | // id - State ID Number 793 | // value - DOM Element to cache 794 | // 795 | // Returns nothing. 796 | function cachePush(id, value) { 797 | cacheMapping[id] = value 798 | cacheBackStack.push(id) 799 | 800 | // Remove all entries in forward history stack after pushing a new page. 801 | trimCacheStack(cacheForwardStack, 0) 802 | 803 | // Trim back history stack to max cache length. 804 | trimCacheStack(cacheBackStack, pjax.defaults.maxCacheLength) 805 | } 806 | 807 | // Shifts cache from directional history cache. Should be 808 | // called on `popstate` with the previous state id and container 809 | // contents. 810 | // 811 | // direction - "forward" or "back" String 812 | // id - State ID Number 813 | // value - DOM Element to cache 814 | // 815 | // Returns nothing. 816 | function cachePop(direction, id, value) { 817 | var pushStack, popStack 818 | cacheMapping[id] = value 819 | 820 | if (direction === 'forward') { 821 | pushStack = cacheBackStack 822 | popStack = cacheForwardStack 823 | } else { 824 | pushStack = cacheForwardStack 825 | popStack = cacheBackStack 826 | } 827 | 828 | pushStack.push(id) 829 | if (id = popStack.pop()) 830 | delete cacheMapping[id] 831 | 832 | // Trim whichever stack we just pushed to to max cache length. 833 | trimCacheStack(pushStack, pjax.defaults.maxCacheLength) 834 | } 835 | 836 | // Trim a cache stack (either cacheBackStack or cacheForwardStack) to be no 837 | // longer than the specified length, deleting cached DOM elements as necessary. 838 | // 839 | // stack - Array of state IDs 840 | // length - Maximum length to trim to 841 | // 842 | // Returns nothing. 843 | function trimCacheStack(stack, length) { 844 | while (stack.length > length) 845 | delete cacheMapping[stack.shift()] 846 | } 847 | 848 | // Public: Find version identifier for the initial page load. 849 | // 850 | // Returns String version or undefined. 851 | function findVersion() { 852 | return $('meta').filter(function() { 853 | var name = $(this).attr('http-equiv') 854 | return name && name.toUpperCase() === 'X-PJAX-VERSION' 855 | }).attr('content') 856 | } 857 | 858 | // Install pjax functions on $.pjax to enable pushState behavior. 859 | // 860 | // Does nothing if already enabled. 861 | // 862 | // Examples 863 | // 864 | // $.pjax.enable() 865 | // 866 | // Returns nothing. 867 | function enable() { 868 | $.fn.pjax = fnPjax 869 | $.pjax = pjax 870 | $.pjax.enable = $.noop 871 | $.pjax.disable = disable 872 | $.pjax.click = handleClick 873 | $.pjax.submit = handleSubmit 874 | $.pjax.reload = pjaxReload 875 | $.pjax.defaults = { 876 | timeout: 650, 877 | push: true, 878 | replace: false, 879 | type: 'GET', 880 | dataType: 'html', 881 | scrollTo: 0, 882 | maxCacheLength: 20, 883 | version: findVersion 884 | } 885 | $(window).on('popstate.pjax', onPjaxPopstate) 886 | } 887 | 888 | // Disable pushState behavior. 889 | // 890 | // This is the case when a browser doesn't support pushState. It is 891 | // sometimes useful to disable pushState for debugging on a modern 892 | // browser. 893 | // 894 | // Examples 895 | // 896 | // $.pjax.disable() 897 | // 898 | // Returns nothing. 899 | function disable() { 900 | $.fn.pjax = function() { return this } 901 | $.pjax = fallbackPjax 902 | $.pjax.enable = enable 903 | $.pjax.disable = $.noop 904 | $.pjax.click = $.noop 905 | $.pjax.submit = $.noop 906 | $.pjax.reload = function() { window.location.reload() } 907 | 908 | $(window).off('popstate.pjax', onPjaxPopstate) 909 | } 910 | 911 | 912 | // Add the state property to jQuery's event object so we can use it in 913 | // $(window).bind('popstate') 914 | if ( $.inArray('state', $.event.props) < 0 ) 915 | $.event.props.push('state') 916 | 917 | // Is pjax supported by this browser? 918 | $.support.pjax = 919 | window.history && window.history.pushState && window.history.replaceState && 920 | // pushState isn't reliable on iOS until 5. 921 | !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/) 922 | 923 | $.support.pjax ? enable() : disable() 924 | 925 | })(jQuery); 926 | --------------------------------------------------------------------------------