· 4 years ago · Aug 23, 2021, 05:52 PM
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 }
39 handleClick(event, opts);
40 });
41 }
42
43 // Public: pjax on click handler
44 //
45 // Exported as $.pjax.click.
46 //
47 // event - "click" jQuery.Event
48 // options - pjax options
49 //
50 // Examples
51 //
52 // $(document).on('click', 'a', $.pjax.click)
53 // // is the same as
54 // $(document).pjax('a')
55 //
56 // $(document).on('click', 'a', function(event) {
57 // var container = $(this).closest('[data-pjax-container]')
58 // $.pjax.click(event, container)
59 // })
60 //
61 // Returns nothing.
62 function handleClick(event, container, options) {
63 options = optionsFor(container, options);
64
65 var link = event.currentTarget;
66
67 if (link.tagName.toUpperCase() !== 'A') {
68 throw '$.fn.pjax or $.pjax.click requires an anchor element';
69 }
70
71 // Middle click, cmd click, and ctrl click should open
72 // links in a new tab as normal.
73 if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
74 return;
75 }
76
77 // Ignore cross origin links
78 if (location.protocol !== link.protocol || location.hostname !== link.hostname) {
79 return;
80 }
81
82 // Ignore case when a hash is being tacked on the current URL
83 if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location)) {
84 return;
85 }
86
87 // Ignore event with default prevented
88 if (event.isDefaultPrevented()) {
89 return;
90 }
91
92 var defaults = {
93 url : link.href,
94 container: $(link).attr('data-pjax'),
95 target : link
96 };
97
98 var opts = $.extend({}, defaults, options);
99 var clickEvent = $.Event('pjax:click');
100 $(link).trigger(clickEvent, [opts]);
101
102 if (!clickEvent.isDefaultPrevented()) {
103 pjax(opts);
104 event.preventDefault();
105 $(link).trigger('pjax:clicked', [opts]);
106 }
107 }
108
109 // Public: pjax on form submit handler
110 //
111 // Exported as $.pjax.submit
112 //
113 // event - "click" jQuery.Event
114 // options - pjax options
115 //
116 // Examples
117 //
118 // $(document).on('submit', 'form', function(event) {
119 // var container = $(this).closest('[data-pjax-container]')
120 // $.pjax.submit(event, container)
121 // })
122 //
123 // Returns nothing.
124 function handleSubmit(event, container, options) {
125 options = optionsFor(container, options);
126
127 var form = event.currentTarget;
128 var $form = $(form);
129
130 if (form.tagName.toUpperCase() !== 'FORM') {
131 throw '$.pjax.submit requires a form element';
132 }
133
134 var defaults = {
135 type : ($form.attr('method') || 'GET').toUpperCase(),
136 url : $form.attr('action'),
137 container: $form.attr('data-pjax'),
138 target : form
139 };
140
141 if (defaults.type !== 'GET' && window.FormData !== undefined) {
142 defaults.data = new FormData(form);
143 defaults.processData = false;
144 defaults.contentType = false;
145 } else {
146 // Can't handle file uploads, exit
147 if ($(form).find(':file').length) {
148 return;
149 }
150
151 // Fallback to manually serializing the fields
152 defaults.data = $(form).serializeArray();
153 }
154
155 pjax($.extend({}, defaults, options));
156
157 event.preventDefault();
158 }
159
160 // Loads a URL with ajax, puts the response body inside a container,
161 // then pushState()'s the loaded URL.
162 //
163 // Works just like $.ajax in that it accepts a jQuery ajax
164 // settings object (with keys like url, type, data, etc).
165 //
166 // Accepts these extra keys:
167 //
168 // container - Where to stick the response body.
169 // $(container).html(xhr.responseBody)
170 // push - Whether to pushState the URL. Defaults to true (of course).
171 // replace - Want to use replaceState instead? That's cool.
172 //
173 // Use it just like $.ajax:
174 //
175 // var xhr = $.pjax({ url: this.href, container: '#main' })
176 // console.log( xhr.readyState )
177 //
178 // Returns whatever $.ajax returns.
179 function pjax(options) {
180 options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options);
181
182 if (typeof options.url === 'function') {
183 options.url = options.url();
184 }
185
186 var target = options.target;
187
188 var hash = parseURL(options.url).hash;
189
190 var _container = findContainerFor(options.container);
191 var context = options.context = _container[0];
192 var selector = _container[1];
193
194 // We want the browser to maintain two separate internal caches: one
195 // for pjax'd partial page loads and one for normal page loads.
196 // Without adding this secret parameter, some browsers will often
197 // confuse the two.
198 if (!options.data) {
199 options.data = {};
200 }
201 if ($.isArray(options.data)) {
202 options.data.push({
203 name : '_pjax',
204 value: selector
205 });
206 } else {
207 options.data._pjax = selector;
208 }
209
210 function fire(type, args, props) {
211 if (!props) {
212 props = {};
213 }
214 props.relatedTarget = target;
215 var event = $.Event(type, props);
216 context.trigger(event, args);
217 return !event.isDefaultPrevented();
218 }
219
220 var timeoutTimer;
221
222 options.beforeSend = function(xhr, settings) {
223 // No timeout for non-GET requests
224 // Its not safe to request the resource again with a fallback method.
225 if (settings.type !== 'GET') {
226 settings.timeout = 0;
227 }
228
229 xhr.setRequestHeader('X-PJAX', 'true');
230 xhr.setRequestHeader('X-PJAX-Container', selector);
231
232 if (!fire('pjax:beforeSend', [
233 xhr,
234 settings
235 ])) {
236 return false;
237 }
238
239 if (settings.timeout > 0) {
240 timeoutTimer = setTimeout(function() {
241 if (fire('pjax:timeout', [
242 xhr,
243 options
244 ])) {
245 xhr.abort('timeout');
246 }
247 }, settings.timeout);
248
249 // Clear timeout setting so jquerys internal timeout isn't invoked
250 settings.timeout = 0;
251 }
252
253 var url = parseURL(settings.url);
254 if (hash) {
255 url.hash = hash;
256 }
257 options.requestUrl = stripInternalParams(url);
258 };
259
260 options.complete = function(xhr, textStatus) {
261 if (timeoutTimer) {
262 clearTimeout(timeoutTimer);
263 }
264
265 fire('pjax:complete', [
266 xhr,
267 textStatus,
268 options
269 ]);
270
271 fire('pjax:end', [
272 xhr,
273 options
274 ]);
275 };
276
277 options.error = function(xhr, textStatus, errorThrown) {
278 var container = extractContainer('', xhr, options);
279
280 var allowed = fire('pjax:error', [
281 xhr,
282 textStatus,
283 errorThrown,
284 options
285 ]);
286 if (options.type == 'GET' && textStatus !== 'abort' && allowed) {
287 locationReplace(container.url);
288 }
289 };
290
291 options.success = function(data, status, xhr) {
292 var previousState = pjax.state;
293
294 // If $.pjax.defaults.version is a function, invoke it first.
295 // Otherwise it can be a static string.
296 var currentVersion = (typeof $.pjax.defaults.version === 'function') ?
297 $.pjax.defaults.version() :
298 $.pjax.defaults.version;
299
300 var latestVersion = xhr.getResponseHeader('X-PJAX-Version');
301
302 var container = extractContainer(data, xhr, options);
303
304 var url = parseURL(container.url);
305 if (hash) {
306 url.hash = hash;
307 container.url = url.href;
308 }
309
310 // If there is a layout version mismatch, hard load the new url
311 if (currentVersion && latestVersion && currentVersion !== latestVersion) {
312 locationReplace(container.url);
313 return;
314 }
315
316 // If the new response is missing a body, hard load the page
317 if (!container.contents) {
318 locationReplace(container.url);
319 return;
320 }
321
322 pjax.state = {
323 id : options.id || uniqueId(),
324 url : container.url,
325 title : container.title,
326 container: selector,
327 fragment : options.fragment,
328 timeout : options.timeout
329 };
330
331 if (options.push || options.replace) {
332 window.history.replaceState(pjax.state, container.title, container.url);
333 }
334
335 // Only blur the focus if the focused element is within the container.
336 var blurFocus = $.contains(options.container, document.activeElement);
337
338 // Clear out any focused controls before inserting new page contents.
339 if (blurFocus) {
340 try {
341 document.activeElement.blur();
342 }
343 catch (e) {
344 }
345 }
346
347 if (container.title) {
348 document.title = container.title;
349 }
350
351 fire('pjax:beforeReplace', [
352 container.contents,
353 options
354 ], {
355 state : pjax.state,
356 previousState: previousState
357 });
358
359 if ('function' === typeof options.renderCallback) {
360 options.renderCallback(context, container.contents, afterRender);
361 } else {
362 context.html(container.contents);
363 afterRender();
364 }
365
366 function afterRender() {
367 // FF bug: Won't autofocus fields that are inserted via JS.
368 // This behavior is incorrect. So if theres no current focus, autofocus
369 // the last field.
370 //
371 // http://www.w3.org/html/wg/drafts/html/master/forms.html
372 var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0];
373 if (autofocusEl && document.activeElement !== autofocusEl) {
374 autofocusEl.focus();
375 }
376
377 executeScriptTags(container.scripts);
378
379 var scrollTo = options.scrollTo;
380
381 // Ensure browser scrolls to the element referenced by the URL anchor
382 if (hash) {
383 var name = decodeURIComponent(hash.slice(1));
384 var target = document.getElementById(name) || document.getElementsByName(name)[0];
385 if (target) {
386 scrollTo = $(target).offset().top;
387 }
388 }
389
390 if (typeof scrollTo == 'number') {
391 $(window).scrollTop(scrollTo);
392 }
393
394 fire('pjax:success', [
395 data,
396 status,
397 xhr,
398 options
399 ]);
400 }
401 };
402
403 // Initialize pjax.state for the initial page load. Assume we're
404 // using the container and options of the link we're loading for the
405 // back button to the initial page. This ensures good back button
406 // behavior.
407 if (!pjax.state) {
408 pjax.state = {
409 id : uniqueId(),
410 url : window.location.href,
411 title : document.title,
412 container: selector,
413 fragment : options.fragment,
414 timeout : options.timeout
415 };
416 window.history.replaceState(pjax.state, document.title);
417 }
418
419 // Cancel the current request if we're already pjaxing
420 abortXHR(pjax.xhr);
421
422 pjax.options = options;
423 var xhr = pjax.xhr = $.ajax(options);
424
425 if (xhr.readyState > 0) {
426 if (options.push && !options.replace) {
427 // Cache current container element before replacing it
428 cachePush(pjax.state.id, cloneContents(context, selector));
429
430 window.history.pushState(null, '', options.requestUrl);
431 }
432
433 fire('pjax:start', [
434 xhr,
435 options
436 ]);
437 fire('pjax:send', [
438 xhr,
439 options
440 ]);
441 }
442
443 return pjax.xhr;
444 }
445
446 // Public: Reload current page with pjax.
447 //
448 // Returns whatever $.pjax returns.
449 function pjaxReload(container, options) {
450 var defaults = {
451 url : window.location.href,
452 push : false,
453 replace : true,
454 scrollTo: false
455 };
456
457 return pjax($.extend(defaults, optionsFor(container, options)));
458 }
459
460 // Internal: Hard replace current state with url.
461 //
462 // Work for around WebKit
463 // https://bugs.webkit.org/show_bug.cgi?id=93506
464 //
465 // Returns nothing.
466 function locationReplace(url) {
467 window.history.replaceState(null, '', pjax.state.url);
468 window.location.replace(url);
469 }
470
471 var initialPop = true;
472 var initialURL = window.location.href;
473 var initialState = window.history.state;
474
475 // Initialize $.pjax.state if possible
476 // Happens when reloading a page and coming forward from a different
477 // session history.
478 if (initialState && initialState.container) {
479 pjax.state = initialState;
480 }
481
482 // Non-webkit browsers don't fire an initial popstate event
483 if ('state' in window.history) {
484 initialPop = false;
485 }
486
487 // popstate handler takes care of the back and forward buttons
488 //
489 // You probably shouldn't use pjax on pages with other pushState
490 // stuff yet.
491 function onPjaxPopstate(event) {
492
493 // Hitting back or forward should override any pending PJAX request.
494 if (!initialPop) {
495 abortXHR(pjax.xhr);
496 }
497
498 var previousState = pjax.state;
499 var state = event.state;
500 var direction;
501
502 if (state && state.container) {
503 // When coming forward from a separate history session, will get an
504 // initial pop with a state we are already at. Skip reloading the current
505 // page.
506 if (initialPop && initialURL == state.url) {
507 return;
508 }
509
510 if (previousState) {
511 // If popping back to the same state, just skip.
512 // Could be clicking back from hashchange rather than a pushState.
513 if (previousState.id === state.id) {
514 return;
515 }
516
517 // Since state IDs always increase, we can deduce the navigation direction
518 direction = previousState.id < state.id ? 'forward' : 'back';
519 }
520
521 var cache = cacheMapping[state.id] || [];
522 var selector = cache[0] || state.container;
523 var container = $(selector), contents = cache[1];
524
525 if (container.length) {
526 if (previousState) {
527 // Cache current container before replacement and inform the
528 // cache which direction the history shifted.
529 cachePop(direction, previousState.id, cloneContents(container, selector));
530 }
531
532 var popstateEvent = $.Event('pjax:popstate', {
533 state : state,
534 direction: direction
535 });
536 container.trigger(popstateEvent);
537
538 var options = {
539 id : state.id,
540 url : state.url,
541 container: container,
542 push : false,
543 fragment : state.fragment,
544 timeout : state.timeout,
545 scrollTo : false
546 };
547
548 if (contents) {
549 container.trigger('pjax:start', [
550 null,
551 options
552 ]);
553
554 pjax.state = state;
555 if (state.title) {
556 document.title = state.title;
557 }
558 var beforeReplaceEvent = $.Event('pjax:beforeReplace', {
559 state : state,
560 previousState: previousState
561 });
562 container.trigger(beforeReplaceEvent, [
563 contents,
564 options
565 ]);
566 container.html(contents);
567
568 container.trigger('pjax:end', [
569 null,
570 options
571 ]);
572 } else {
573 pjax(options);
574 }
575
576 // Force reflow/relayout before the browser tries to restore the
577 // scroll position.
578 container[0].offsetHeight;
579 } else {
580 locationReplace(location.href);
581 }
582 }
583 initialPop = false;
584 }
585
586 // Fallback version of main pjax function for browsers that don't
587 // support pushState.
588 //
589 // Returns nothing since it retriggers a hard form submission.
590 function fallbackPjax(options) {
591 var url = typeof options.url === 'function' ? options.url() : options.url,
592 method = options.type ? options.type.toUpperCase() : 'GET';
593
594 var form = $('<form>', {
595 method: method === 'GET' ? 'GET' : 'POST',
596 action: url,
597 style : 'display:none'
598 });
599
600 if (method !== 'GET' && method !== 'POST') {
601 form.append($('<input>', {
602 type : 'hidden',
603 name : '_method',
604 value: method.toLowerCase()
605 }));
606 }
607
608 var data = options.data;
609 if (typeof data === 'string') {
610 $.each(data.split('&'), function(index, value) {
611 var pair = value.split('=');
612 form.append($('<input>', {
613 type : 'hidden',
614 name : pair[0],
615 value: pair[1]
616 }));
617 });
618 } else if ($.isArray(data)) {
619 $.each(data, function(index, value) {
620 form.append($('<input>', {
621 type : 'hidden',
622 name : value.name,
623 value: value.value
624 }));
625 });
626 } else if (typeof data === 'object') {
627 var key;
628 for (key in data) {
629 form.append($('<input>', {
630 type : 'hidden',
631 name : key,
632 value: data[key]
633 }));
634 }
635 }
636
637 $(document.body).append(form);
638 form.submit();
639 }
640
641 // Internal: Abort an XmlHttpRequest if it hasn't been completed,
642 // also removing its event handlers.
643 function abortXHR(xhr) {
644 if (xhr && xhr.readyState < 4) {
645 xhr.onreadystatechange = $.noop;
646 xhr.abort();
647 }
648 }
649
650 // Internal: Generate unique id for state object.
651 //
652 // Use a timestamp instead of a counter since ids should still be
653 // unique across page loads.
654 //
655 // Returns Number.
656 function uniqueId() {
657 return (new Date).getTime();
658 }
659
660 function cloneContents(container, selector) {
661 var cloned = container.clone();
662 // Unmark script tags as already being eval'd so they can get executed again
663 // when restored from cache. HAXX: Uses jQuery internal method.
664 cloned.find('script').each(function() {
665 if (!this.src) {
666 jQuery._data(this, 'globalEval', false);
667 }
668 });
669 return [
670 selector,
671 cloned.contents()
672 ];
673 }
674
675 // Internal: Strip internal query params from parsed URL.
676 //
677 // Returns sanitized url.href String.
678 function stripInternalParams(url) {
679 url.search = url.search.replace(/([?&])(_pjax|_)=[^&]*/g, '');
680 return url.href.replace(/\?($|#)/, '$1');
681 }
682
683 // Internal: Parse URL components and returns a Locationish object.
684 //
685 // url - String URL
686 //
687 // Returns HTMLAnchorElement that acts like Location.
688 function parseURL(url) {
689 var a = document.createElement('a');
690 a.href = url;
691 return a;
692 }
693
694 // Internal: Return the `href` component of given URL object with the hash
695 // portion removed.
696 //
697 // location - Location or HTMLAnchorElement
698 //
699 // Returns String
700 function stripHash(location) {
701 return location.href.replace(/#.*/, '');
702 }
703
704 // Internal: Build options Object for arguments.
705 //
706 // For convenience the first parameter can be either the container or
707 // the options object.
708 //
709 // Examples
710 //
711 // optionsFor('#container')
712 // // => {container: '#container'}
713 //
714 // optionsFor('#container', {push: true})
715 // // => {container: '#container', push: true}
716 //
717 // optionsFor({container: '#container', push: true})
718 // // => {container: '#container', push: true}
719 //
720 // Returns options Object.
721 function optionsFor(container, options) {
722 // Both container and options
723 if (container && options) {
724 options.container = container;
725 }// First argument is options Object
726 else if ($.isPlainObject(container)) {
727 options = container;
728 }// Only container
729 else {
730 options = {container: container};
731 }
732
733 // Find and validate container
734 if (options.container) {
735 options.container = findContainerFor(options.container);
736 }
737
738 return options;
739 }
740
741 // Internal: Find container element for a variety of inputs.
742 //
743 // Because we can't persist elements using the history API, we must be
744 // able to find a String selector that will consistently find the Element.
745 //
746 // container - A selector String, jQuery object, or DOM Element.
747 //
748 // Returns a jQuery object whose context is `document` and has a selector.
749 function findContainerFor(container) {
750 var selector, $container;
751 if ($.isArray(container)) {
752 $container = container[0];
753 selector = container[1];
754 } else {
755 selector = container;
756 $container = $(selector);
757 }
758
759 if (!$container.length) {
760 throw 'no pjax container for ' + selector;
761 } else if (true) {
762 return [
763 $container,
764 selector
765 ];
766 } else if (container.selector !== '' && container.context === document) {
767 return container;
768 } else if (container.attr('id')) {
769 return $('#' + container.attr('id'));
770 } else {
771 throw 'cant get selector for pjax container!';
772 }
773 }
774
775 // Internal: Filter and find all elements matching the selector.
776 //
777 // Where $.fn.find only matches descendants, findAll will test all the
778 // top level elements in the jQuery object as well.
779 //
780 // elems - jQuery object of Elements
781 // selector - String selector to match
782 //
783 // Returns a jQuery object.
784 function findAll(elems, selector) {
785 return elems.filter(selector).add(elems.find(selector));
786 }
787
788 function parseHTML(html) {
789 return $.parseHTML(html, document, true);
790 }
791
792 // Internal: Extracts container and metadata from response.
793 //
794 // 1. Extracts X-PJAX-URL header if set
795 // 2. Extracts inline <title> tags
796 // 3. Builds response Element and extracts fragment if set
797 //
798 // data - String response data
799 // xhr - XHR response
800 // options - pjax options Object
801 //
802 // Returns an Object with url, title, and contents keys.
803 function extractContainer(data, xhr, options) {
804 var obj = {}, fullDocument = /<html/i.test(data);
805
806 // Prefer X-PJAX-URL header if it was set, otherwise fallback to
807 // using the original requested url.
808 var serverUrl = xhr.getResponseHeader('X-PJAX-URL');
809 obj.url = serverUrl ? stripInternalParams(parseURL(serverUrl)) : options.requestUrl;
810
811 // Attempt to parse response html into elements
812 if (fullDocument) {
813 var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]));
814 var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]));
815 } else {
816 var $head = $body = $(parseHTML(data));
817 }
818
819 // If response data is empty, return fast
820 if ($body.length === 0) {
821 return obj;
822 }
823
824 // If there's a <title> tag in the header, use it as
825 // the page's title.
826 obj.title = findAll($head, 'title').last().text();
827
828 if (options.fragment) {
829 // If they specified a fragment, look for it in the response
830 // and pull it out.
831 if (options.fragment === 'body') {
832 var $fragment = $body;
833 } else {
834 var $fragment = findAll($body, options.fragment).first();
835 }
836
837 if ($fragment.length) {
838 obj.contents = options.fragment === 'body' ? $fragment : $fragment.contents();
839
840 // If there's no title, look for data-title and title attributes
841 // on the fragment
842 if (!obj.title) {
843 obj.title = $fragment.attr('title') || $fragment.data('title');
844 }
845 }
846
847 } else if (!fullDocument) {
848 obj.contents = $body;
849 }
850
851 // Clean up any <title> tags
852 if (obj.contents) {
853 // Remove any parent title elements
854 obj.contents = obj.contents.not(function() { return $(this).is('title'); });
855
856 // Then scrub any titles from their descendants
857 obj.contents.find('title').remove();
858
859 // Gather all script[src] elements
860 obj.scripts = findAll(obj.contents, 'script[src]').remove();
861 obj.contents = obj.contents.not(obj.scripts);
862 }
863
864 // Trim any whitespace off the title
865 if (obj.title) {
866 obj.title = $.trim(obj.title);
867 }
868
869 return obj;
870 }
871
872 // Load an execute scripts using standard script request.
873 //
874 // Avoids jQuery's traditional $.getScript which does a XHR request and
875 // globalEval.
876 //
877 // scripts - jQuery object of script Elements
878 //
879 // Returns nothing.
880 function executeScriptTags(scripts) {
881 if (!scripts) {
882 return;
883 }
884
885 var existingScripts = $('script[src]');
886
887 scripts.each(function() {
888 var src = this.src;
889 var matchedScripts = existingScripts.filter(function() {
890 return this.src === src;
891 });
892 if (matchedScripts.length) {
893 return;
894 }
895
896 var script = document.createElement('script');
897 var type = $(this).attr('type');
898 if (type) {
899 script.type = type;
900 }
901 script.src = $(this).attr('src');
902 document.head.appendChild(script);
903 });
904 }
905
906 // Internal: History DOM caching class.
907 var cacheMapping = {};
908 var cacheForwardStack = [];
909 var cacheBackStack = [];
910
911 // Push previous state id and container contents into the history
912 // cache. Should be called in conjunction with `pushState` to save the
913 // previous container contents.
914 //
915 // id - State ID Number
916 // value - DOM Element to cache
917 //
918 // Returns nothing.
919 function cachePush(id, value) {
920 cacheMapping[id] = value;
921 cacheBackStack.push(id);
922
923 // Remove all entries in forward history stack after pushing a new page.
924 trimCacheStack(cacheForwardStack, 0);
925
926 // Trim back history stack to max cache length.
927 trimCacheStack(cacheBackStack, pjax.defaults.maxCacheLength);
928 }
929
930 // Shifts cache from directional history cache. Should be
931 // called on `popstate` with the previous state id and container
932 // contents.
933 //
934 // direction - "forward" or "back" String
935 // id - State ID Number
936 // value - DOM Element to cache
937 //
938 // Returns nothing.
939 function cachePop(direction, id, value) {
940 var pushStack, popStack;
941 cacheMapping[id] = value;
942
943 if (direction === 'forward') {
944 pushStack = cacheBackStack;
945 popStack = cacheForwardStack;
946 } else {
947 pushStack = cacheForwardStack;
948 popStack = cacheBackStack;
949 }
950
951 pushStack.push(id);
952 if (id = popStack.pop()) {
953 delete cacheMapping[id];
954 }
955
956 // Trim whichever stack we just pushed to to max cache length.
957 trimCacheStack(pushStack, pjax.defaults.maxCacheLength);
958 }
959
960 // Trim a cache stack (either cacheBackStack or cacheForwardStack) to be no
961 // longer than the specified length, deleting cached DOM elements as necessary.
962 //
963 // stack - Array of state IDs
964 // length - Maximum length to trim to
965 //
966 // Returns nothing.
967 function trimCacheStack(stack, length) {
968 while (stack.length > length) {
969 delete cacheMapping[stack.shift()];
970 }
971 }
972
973 // Public: Find version identifier for the initial page load.
974 //
975 // Returns String version or undefined.
976 function findVersion() {
977 return $('meta').filter(function() {
978 var name = $(this).attr('http-equiv');
979 return name && name.toUpperCase() === 'X-PJAX-VERSION';
980 }).attr('content');
981 }
982
983 // Install pjax functions on $.pjax to enable pushState behavior.
984 //
985 // Does nothing if already enabled.
986 //
987 // Examples
988 //
989 // $.pjax.enable()
990 //
991 // Returns nothing.
992 function enable() {
993 $.fn.pjax = fnPjax;
994 $.pjax = pjax;
995 $.pjax.enable = $.noop;
996 $.pjax.disable = disable;
997 $.pjax.click = handleClick;
998 $.pjax.submit = handleSubmit;
999 $.pjax.reload = pjaxReload;
1000 $.pjax.defaults = {
1001 timeout : 650,
1002 push : true,
1003 replace : false,
1004 type : 'GET',
1005 dataType : 'html',
1006 scrollTo : 0,
1007 renderCallback: false,
1008 maxCacheLength: 20,
1009 version : findVersion
1010 };
1011 $(window).on('popstate.pjax', onPjaxPopstate);
1012 }
1013
1014 // Disable pushState behavior.
1015 //
1016 // This is the case when a browser doesn't support pushState. It is
1017 // sometimes useful to disable pushState for debugging on a modern
1018 // browser.
1019 //
1020 // Examples
1021 //
1022 // $.pjax.disable()
1023 //
1024 // Returns nothing.
1025 function disable() {
1026 $.fn.pjax = function() { return this; };
1027 $.pjax = fallbackPjax;
1028 $.pjax.enable = enable;
1029 $.pjax.disable = $.noop;
1030 $.pjax.click = $.noop;
1031 $.pjax.submit = $.noop;
1032 $.pjax.reload = function() { window.location.reload(); };
1033
1034 $(window).off('popstate.pjax', onPjaxPopstate);
1035 }
1036
1037 // Add the state property to jQuery's event object so we can use it in
1038 // $(window).bind('popstate')
1039 if ($.event.props && $.inArray('state', $.event.props) < 0) {
1040 $.event.props.push('state');
1041 } else if (!('state' in $.Event.prototype)) {
1042 $.event.addProp('state');
1043 }
1044
1045 // Is pjax supported by this browser?
1046 $.support.pjax =
1047 window.history && window.history.pushState && window.history.replaceState &&
1048 // pushState isn't reliable on iOS until 5.
1049 !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/)
1050
1051 $.support.pjax ? enable() : disable()
1052
1053})(jQuery);