Skip to content

Instantly share code, notes, and snippets.

@balupton
Last active April 20, 2022 13:21
Show Gist options
  • Save balupton/854622 to your computer and use it in GitHub Desktop.
Save balupton/854622 to your computer and use it in GitHub Desktop.
Ajaxify a Website with the HTML5 History API using History.js, jQuery and ScrollTo
// History.js It!
// v1.0.1 - 30 September, 2012
// https://gist.github.com/854622
(function(window,undefined){
// Prepare our Variables
var
History = window.History,
$ = window.jQuery,
document = window.document;
// Check to see if History.js is enabled for our Browser
if ( !History.enabled ) {
return false;
}
// Wait for Document
$(function(){
// Prepare Variables
var
/* Application Specific Variables */
contentSelector = '#content,article:first,.article:first,.post:first',
$content = $(contentSelector).filter(':first'),
contentNode = $content.get(0),
$menu = $('#menu,#nav,nav:first,.nav:first').filter(':first'),
activeClass = 'active selected current youarehere',
activeSelector = '.active,.selected,.current,.youarehere',
menuChildrenSelector = '> li,> ul > li',
completedEventName = 'statechangecomplete',
/* Application Generic Variables */
$window = $(window),
$body = $(document.body),
rootUrl = History.getRootUrl(),
scrollOptions = {
duration: 800,
easing:'swing'
};
// Ensure Content
if ( $content.length === 0 ) {
$content = $body;
}
// Internal Helper
$.expr[':'].internal = function(obj, index, meta, stack){
// Prepare
var
$this = $(obj),
url = $this.attr('href')||'',
isInternalLink;
// Check link
isInternalLink = url.substring(0,rootUrl.length) === rootUrl || url.indexOf(':') === -1;
// Ignore or Keep
return isInternalLink;
};
// HTML Helper
var documentHtml = function(html){
// Prepare
var result = String(html)
.replace(/<\!DOCTYPE[^>]*>/i, '')
.replace(/<(html|head|body|title|meta|script)([\s\>])/gi,'<div class="document-$1"$2')
.replace(/<\/(html|head|body|title|meta|script)\>/gi,'</div>')
;
// Return
return result;
};
// Ajaxify Helper
$.fn.ajaxify = function(){
// Prepare
var $this = $(this);
// Ajaxify
$this.find('a:internal:not(.no-ajaxy)').click(function(event){
// Prepare
var
$this = $(this),
url = $this.attr('href'),
title = $this.attr('title')||null;
// Continue as normal for cmd clicks etc
if ( event.which == 2 || event.metaKey ) { return true; }
// Ajaxify this link
History.pushState(null,title,url);
event.preventDefault();
return false;
});
// Chain
return $this;
};
// Ajaxify our Internal Links
$body.ajaxify();
// Hook into State Changes
$window.bind('statechange',function(){
// Prepare Variables
var
State = History.getState(),
url = State.url,
relativeUrl = url.replace(rootUrl,'');
// Set Loading
$body.addClass('loading');
// Start Fade Out
// Animating to opacity to 0 still keeps the element's height intact
// Which prevents that annoying pop bang issue when loading in new content
$content.animate({opacity:0},800);
// Ajax Request the Traditional Page
$.ajax({
url: url,
success: function(data, textStatus, jqXHR){
// Prepare
var
$data = $(documentHtml(data)),
$dataBody = $data.find('.document-body:first'),
$dataContent = $dataBody.find(contentSelector).filter(':first'),
$menuChildren, contentHtml, $scripts;
// Fetch the scripts
$scripts = $dataContent.find('.document-script');
if ( $scripts.length ) {
$scripts.detach();
}
// Fetch the content
contentHtml = $dataContent.html()||$data.html();
if ( !contentHtml ) {
document.location.href = url;
return false;
}
// Update the menu
$menuChildren = $menu.find(menuChildrenSelector);
$menuChildren.filter(activeSelector).removeClass(activeClass);
$menuChildren = $menuChildren.has('a[href^="'+relativeUrl+'"],a[href^="/'+relativeUrl+'"],a[href^="'+url+'"]');
if ( $menuChildren.length === 1 ) { $menuChildren.addClass(activeClass); }
// Update the content
$content.stop(true,true);
$content.html(contentHtml).ajaxify().css('opacity',100).show(); /* you could fade in here if you'd like */
// Update the title
document.title = $data.find('.document-title:first').text();
try {
document.getElementsByTagName('title')[0].innerHTML = document.title.replace('<','&lt;').replace('>','&gt;').replace(' & ',' &amp; ');
}
catch ( Exception ) { }
// Add the scripts
$scripts.each(function(){
var $script = $(this), scriptText = $script.text(), scriptNode = document.createElement('script');
scriptNode.appendChild(document.createTextNode(scriptText));
contentNode.appendChild(scriptNode);
});
// Complete the change
if ( $body.ScrollTo||false ) { $body.ScrollTo(scrollOptions); } /* http://balupton.com/projects/jquery-scrollto */
$body.removeClass('loading');
$window.trigger(completedEventName);
// Inform Google Analytics of the change
if ( typeof window._gaq !== 'undefined' ) {
window._gaq.push(['_trackPageview', relativeUrl]);
}
// Inform ReInvigorate of a state change
if ( typeof window.reinvigorate !== 'undefined' && typeof window.reinvigorate.ajax_track !== 'undefined' ) {
reinvigorate.ajax_track(url);
// ^ we use the full url here as that is what reinvigorate supports
}
},
error: function(jqXHR, textStatus, errorThrown){
document.location.href = url;
return false;
}
}); // end ajax
}); // end onStateChange
}); // end onDomLoad
})(window); // end closure
@sieppl
Copy link

sieppl commented Nov 8, 2012

Great stuff. Please check (and merge) my gist: https://gist.github.com/4041997
It adds that also all links where any parent has the class "no-ajaxy" are exluded. Makes excluding e.g. navbars entirely more comfortable.

@fahimshani
Copy link

Hi just wondering how to make it work if the page struture is like (index.html, about.html, and bla bla) in same folder, do i have to make some configuration in the gist, sorry for the silly question but i am trying for last 2 days to make it work for static website pure html

Thanks

@juji
Copy link

juji commented Dec 19, 2012

i use ajaxify-html5.js, and have inline scripts in my html.

<script>
    for( var i = 0; i < 10; i++ ){ }
</script>

resulted in an error...

so i change the code.. (it's a mess though..)

i wonder if anyone have a better solution, i really need the help...

the code (edited):
the followings are inside the $window.bind('statechange',function(){ ...
starting from line 117

    // Ajax Request the Traditional Page
$.ajax({
    url: url,
    success: function(data, textStatus, jqXHR){
        // get inline script
        var body = data.match(/<body[^>]*?>[^]*?<\/body>/img);
        var inlinescript = body[0].match(/<script[^>]*?>[^]*?<\/script>/img);

        // replace inline script with '', 
        data = data.replace(/<script[^>]*?>[^]*?<\/script>/img,'');

        // Prepare
        var
            $data = $(documentHtml(data)),
            $dataBody = $data.find('.document-body:first'),
            $dataContent = $dataBody.find(contentSelector).filter(':first'),
            $menuChildren, contentHtml, $scripts;

        ...

        // Add the scripts
        $scripts.each(function(){
                var $script = $(this), scriptText = $script.text(), 
                scriptNode = document.createElement('script');
                scriptNode.appendChild(document.createTextNode(scriptText));
                contentNode.appendChild(scriptNode);
        });

        // add the inline script
        $(inlinescript).each(function(){
            var scriptText = this.replace(/<script[^]*?>/,'').replace(/<\/script>/,''), 
            scriptNode = document.createElement('script');
            scriptNode.appendChild(document.createTextNode(scriptText));
            contentNode.appendChild(scriptNode);
        });

        ...
    } //end of ajax success function

:)

@greencmg
Copy link

greencmg commented Jan 3, 2013

This a great script! Love it!

I did run into an issue when implemented with the new Google Tag Manager (GTM) with Google Analytics. If using GTM the _gaq.push event still fires but there is no link to an account id. You will see the event fire but it's posting to UA-XXXXX-XX. To quickly work around this, in GTM, I created a rule based on {event} that equals a value of 'pageView'. Then in the ajaxify-html5.js script I changed the following starting at 172:

                // Inform Google Analytics of the change
                if ( typeof window._gaq !== 'undefined' ) {
                    window._gaq.push(['_trackPageview', relativeUrl]);
                }

Change to:

                if ( typeof window._gaq !== 'undefined' ) {
                    dataLayer.push({'event':'pageView'});
                } 

Pageviews are executing normally now. For more info on Google Tag Manager -- check out http://support.google.com/tagmanager/

@drivebass
Copy link

Great script! I have a problem with my home page though that is loading in the container if i hit the back button. What i want is every page except my home page to load inside the container. Is this possible to exclude a page from loading in the container div?

@drivebass
Copy link

Is it possible to exclude specific strings like wp-admin.php, wp-login.php, index.php etc? How can i add a function that when i visit the home page (including with browser's back button) will just hide the $content div?

@drivebass
Copy link

Help anyone i desperately need a solution on this. Please!

@jerryjack
Copy link

should note that this script throws an error if using jquery v1.9.0(latest version)

@genereddick
Copy link

Error under jquery 1.9.0 is on $data = $(documentHtml(data)), and, after failing to figure out the selector, ends up here:
Sizzle.error = function( msg ) {
throw new Error( "Syntax error, unrecognized expression: " + msg );
};

@genereddick
Copy link

In the jQuery 1.9 change log there is this: http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring

In my case, the response returned from documentHtml(data) doesn't start with a < (in my case it is a conditional for modernizr) is rejected by the new jquery selector parser.

changing the var documentHtml = function(html) to return $.parseHtml seems to work for me, though I haven't explored any follow on effects or issues.

// Return
return $.parseHTML(result);

@daslicht
Copy link

You can also use Davis.js to Ajaxify/hijax you webpage:
see: http://78.47.126.11:3000

@supermensa
Copy link

As far as I can see; the script makes an ajax call for the browser back-button and previously clicked links. Can you update this gist to show how to ensure that saved states/pages are retrieved from history instead of making a new ajax request?

@alexlangberg
Copy link

The fix from genereddick works but ScrollTo is still broken with new versions of jQuery, since $.browser has been deprecated. I've tried including the old code for $.browser but it only seems to be working in Firefox.

@PepiSCZ
Copy link

PepiSCZ commented Mar 9, 2013

Why already loaded javascript is not applied to new content in content div?

@Zeokat
Copy link

Zeokat commented Mar 1, 2014

Zeokat you saved my day! thanks. Nice piece of code.

@Shobby
Copy link

Shobby commented Nov 9, 2016

I am facing an issue. I want to run jaquery on the loaded page but it not working because the page is not loaded and jquery not hit i want to show an alert when the page is loaded but not work:
jQuery(document).ready(function() { alert("hello world"); )};

i put this code in footer file but not working

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment