fairplaysk/datacamp

View on GitHub
app/assets/javascripts/jquery.history.js

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * jQuery History Plugin (balupton edition) - Simple History Handler/Remote for Hash, State, Bookmarking, and Forward Back Buttons
 * Copyright (C) 2008-2009 Benjamin Arthur Lupton
 * http://www.balupton/projects/jquery_history/
 *
 * This file is part of jQuery History Plugin (balupton edition).
 * 
 * jQuery History Plugin (balupton edition) is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * jQuery History Plugin (balupton edition) is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with jQuery History Plugin (balupton edition).  If not, see <http://www.gnu.org/licenses/>.
 *
 * @name jqsmarty: jquery.history.js
 * @package jQuery History Plugin (balupton edition)
 * @version 1.0.1-final
 * @date July 11, 2009
 * @category jquery plugin
 * @author Benjamin "balupton" Lupton {@link http://www.balupton.com}
 * @copyright (c) 2008-2009 Benjamin Arthur Lupton {@link http://www.balupton.com}
 * @license GNU Affero General Public License - {@link http://www.gnu.org/licenses/agpl.html}
 * @example Visit {@link http://jquery.com/plugins/project/jquery_history_bal} for more information.
 * 
 * 
 * I would like to take this space to thank the following projects, blogs, articles and people:
 * - jQuery {@link http://jquery.com/}
 * - jQuery UI History - Klaus Hartl {@link http://www.stilbuero.de/jquery/ui_history/}
 * - Really Simple History - Brian Dillard and Brad Neuberg {@link http://code.google.com/p/reallysimplehistory/}
 * - jQuery History Plugin - Taku Sano (Mikage Sawatari) {@link http://www.mikage.to/jquery/jquery_history.html}
 * - jQuery History Remote Plugin - Klaus Hartl {@link http://stilbuero.de/jquery/history/}
 * - Content With Style: Fixing the back button and enabling bookmarking for ajax apps - Mike Stenhouse {@link http://www.contentwithstyle.co.uk/Articles/38/fixing-the-back-button-and-enabling-bookmarking-for-ajax-apps}
 * - Bookmarks and Back Buttons {@link http://ajax.howtosetup.info/options-and-efficiencies/bookmarks-and-back-buttons/}
 * - Ajax: How to handle bookmarks and back buttons - Brad Neuberg {@link http://dev.aol.com/ajax-handling-bookmarks-and-back-button}
 *
 **
 ***
 * CHANGELOG
 **
 * v1.0.1-final, July 11, 2009
 * - Restructured a little bit
 * - Documented
 * - Cleaned go/request
 *
 * v1.0.0-final, June 19, 2009
 * - Been stable for over a year now, pushing live.
 * 
 * v0.1.0-dev, July 24, 2008
 * - Initial Release
 * 
 */

// Start of our jQuery Plugin
(function($)
{    // Create our Plugin function, with $ as the argument (we pass the jQuery object over later)
    // More info: http://docs.jquery.com/Plugins/Authoring#Custom_Alias
    
    // Debug
    if (typeof console === 'undefined') {
        console = typeof window.console !== 'undefined' ? window.console : {};
    }
    console.log            = console.log             || function(){};
    console.debug        = console.debug         || console.log;
    console.warn        = console.warn            || console.log;
    console.error        = console.error            || function(){var args = [];for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); } alert(args.join("\n")); };
    console.trace        = console.trace            || console.log;
    console.group        = console.group            || console.log;
    console.groupEnd    = console.groupEnd        || console.log;
    console.profile        = console.profile        || console.log;
    console.profileEnd    = console.profileEnd    || console.log;
    
    // Declare our class
    $.History = {
        // Our Plugin definition
        
        // -----------------
        // Options
        
        options: {
            debug: false
        },
        
        // -----------------
        // Variables
        
        state:        '',
        $window:    null,
        $iframe:    null,
        handlers:    {
            generic:    [],
            specific:    {}
        },
        
        // --------------------------------------------------
        // Functions
        
        /**
         * Format a hash into a proper state
         * @param {String} hash
         */
        format: function ( hash ) {
            // Format the hash
            hash = hash.replace(/^.+?#/g,'').replace(/^#?\/?|\/?$/g, '');
            // Return the hash
            return hash;
        },
        
        /**
         * Get the current state of the application
         */
        getState: function ( ) {
            var History = $.History;
            // Get the current state
            return History.state;
        },
        /**
         * Set the current state of the application
         * @param {String} hash
         */
        setState: function ( state ) {
            var History = $.History;
            // Format the state
            state = History.format(state)
            // Apply the state
            History.state = state;
            // Return the state
            return History.state;
        },
        
        /**
         * Get the current hash of the browser
         */
        getHash: function ( ) {
            var History = $.History;
            // Get hash
            var hash = window.location.hash || location.hash;
            // Format the hash
            hash = History.format(hash);
            // Return the hash
            return hash;
        },
        /**
         * Set the current hash of the browser
         * @param {String} hash
         */
        setHash: function ( hash ) {
            var History = $.History;
            // Prepare hash
            hash = $.History.format(hash);
            hash = hash.replace(/^\/?|\/?(\?)|\/?$/g, '/$1');
            
            // Write hash
            if ( typeof window.location.hash !== 'undefined' ) {
                window.location.hash = hash;
            } else {
                location.hash = hash;
            }
            
            // Update IE<8 History
            if ( $.browser.msie && parseInt($.browser.version, 10) < 8 )
            {    // We are IE<8
                $.History.$iframe.contentWindow.document.open();
                $.History.$iframe.contentWindow.document.close();
                $.History.$iframe.contentWindow.document.location.hash = state;                        
            }
        },
        
        /**
         * Go to the specific state - does not force a history entry like setHash
         * @param {String} state
         */
        go: function ( state ) {
            var History = $.History;
            
            // Format the state
            state = History.format(state);
            
            // Get the current hash
            var hash = History.getHash();
            
            // Are they different?
            if ( hash !== state ) {
                // Yes, create a history entry
                History.setHash(state);
                // Wait for hashchange
            } else {
                // No change, but update state and fire
                History.setState(state);
                History.trigger();
            }
            
            // Done
            return true;
        },
        
        /**
         * Fired when the hash is changed, either automaticly or manually
         * @param {Event} e
         */
        hashchange: function ( e ) {
            var History = $.History;
            
            // Debug
            if ( History.options.debug ) {
                console.debug('History.hashchange', this, arguments);
            }
            
            // Get Hash
            var hash = History.getHash();
            var state = History.getState();
            
            // Prevent IE 8 from fireing this twice
            if ( (!History.$iframe && state === hash) || (History.$iframe && History.hash === History.$iframe.contentWindow.document.location.hash) ) {
                // For some reason this works
                return false;
            }
            
            // Check
            if ( state === hash ) {
                // Nothing to do
                return false;
            }
            
            // Update the state with the new hash
            History.setState(hash);
            
            // Fire the handler
            History.trigger();
            
            // All done
            return true;
        },
        
        /**
         * Bind a handler to a hash
         * @param {Object} state
         * @param {Object} handler
         */
        bind: function ( state, handler ) {
            var History = $.History;
            
            // 
            if ( handler ) {
                // We have a state specific handler
                // Prepare
                if ( typeof History.handlers.specific[state] === 'undefined' )
                {    // Make it an array
                    History.handlers.specific[state] = [];
                }
                // Push new handler
                History.handlers.specific[state].push(handler);
            }
            else {
                // We have a generic handler
                handler = state;
                History.handlers.generic.push(handler);
            }
            
            // Done
            return true;
        },
        
        /**
         * Trigger a handler for a state
         * @param {String} state
         */
        trigger: function ( state ) {
            var History = $.History;
            
            // Prepare
            if ( typeof state === 'undefined' ) {
                // Use current
                state = History.getState();
            }
            var i, n, handler, list;
            
            // Fire specific
            if ( typeof History.handlers.specific[state] !== 'undefined' ) {
                // We have specific handlers
                list = History.handlers.specific[state];
                for ( i = 0, n = list.length; i < n; ++i ) {
                    // Fire the specific handler
                    handler = list[i];
                    handler(state);
                }
            }
            
            // Fire generics
            list = History.handlers.generic;
            for ( i = 0, n = list.length; i < n; ++i ) {
                // Fire the specific handler
                handler = list[i];
                handler(state);
            }
            
            // Done
            return true;
        },
        
        // --------------------------------------------------
        // Constructors
        
        /**
         * Construct our application
         */
        construct: function ( ) {
            var History = $.History;
            
            // Modify the document
            $(document).ready(function() {
                // Prepare the document
                History.domReady();
            });
            
            // Done
            return true;
        },
        
        /**
         * Configure our application
         * @param {Object} options
         */
        configure: function ( options ) {
            var History = $.History;
            
            // Set options
            History.options = $.extend(History.options, options);
            
            // Done
            return true;
        },
        
        domReadied: false,
        domReady: function ( ) {
            var History = $.History;
            
            // Runonce
            if ( History.domRedied ) {
                return;
            }
            History.domRedied = true;
            
            // Define window
            History.$window = $(window);
            
            // Apply the hashchange function
            History.$window.bind('hashchange', this.hashchange);
            
            // Force hashchange support for all browsers
            setTimeout(History.hashchangeLoader, 200);
            
            // All done
            return true;
        },
        
        /**
         * Enable hashchange for all browsers
         */
        hashchangeLoader: function () {
            var History = $.History;
            
            // More is needed for non IE8 browsers
            if ( !($.browser.msie && parseInt($.browser.version) >= 8) ) {    
                // We are not IE8
            
                // State our checker function, it is used to constantly check the location to detect a change
                var checker;
                
                // Handle depending on the browser
                if ( $.browser.msie ) {
                    // We are still IE
                
                    // Append and $iframe to the document, as $iframes are required for back and forward
                    // Create a hidden $iframe for hash change tracking
                    History.$iframe = $('<iframe id="jquery-history-iframe" style="display: none;"></$iframe>').prependTo(document.body)[0];
                    
                    // Create initial history entry
                    History.$iframe.contentWindow.document.open();
                    History.$iframe.contentWindow.document.close();
                    
                    // Check for initial state
                    var hash = History.getHash();
                    if ( hash ) {
                        // Apply it to the iframe
                        History.$iframe.contentWindow.document.location.hash = hash;
                    }
                    
                    // Define the checker function (for bookmarks)
                    checker = function ( ) {
                        var iframeHash = History.format(History.$iframe.contentWindow.document.location.hash);
                        if ( History.getState() !== iframeHash ) {
                            // Back Button Change
                            History.setHash(History.$iframe.contentWindow.document.location.hash);
                        }
                        var hash = History.getHash();
                        if ( History.getState() !== hash ) {
                            // The has has changed
                            History.go(hash);
                        }
                    };
                }
                else {
                    // We are not IE
                
                    // Define the checker function (for bookmarks, back, forward)
                    checker = function ( ) {
                        var hash = History.getHash();
                        if ( History.getState() !== hash ) {
                            // The has has changed
                            History.go(hash);
                        }
                    };
                }
                
                // Apply the checker function
                setInterval(checker, 200);
            }
            else {
                // We are IE8
                var hash = History.getHash();
                if (hash) {
                    History.$window.trigger('hashchange');
                }
            }
            
            // Done
            return true;
        }
    
    }; // We have finished extending/defining our Plugin

    // --------------------------------------------------
    // Finish up
    
    // Instantiate
    $.History.construct();

// Finished definition

})(jQuery); // We are done with our plugin, so lets call it with jQuery as the argument