├── 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 = $('